pax_global_header00006660000000000000000000000064147641062030014515gustar00rootroot0000000000000052 comment=4169436d2356e54775ee7b43be7e8d0407fd2084 enterprise-certificate-proxy-0.3.6/000077500000000000000000000000001476410620300173425ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/.github/000077500000000000000000000000001476410620300207025ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/.github/workflows/000077500000000000000000000000001476410620300227375ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/.github/workflows/test-client.yml000066400000000000000000000010701476410620300257130ustar00rootroot00000000000000name: Build and Test Client on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Build run: go build -v ./client/... - name: Test run: go test -v ./client/... - name: Lint uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: ./client args: -E gofmt --max-same-issues 0 enterprise-certificate-proxy-0.3.6/.github/workflows/test-cshared.yml000066400000000000000000000006631476410620300260550ustar00rootroot00000000000000name: Build and Test C-Shared Library on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Build run: go build -buildmode=c-shared -v -o signer.so ./cshared/... - name: Test run: go test -v ./cshared/... enterprise-certificate-proxy-0.3.6/.github/workflows/test-signer-darwin.yml000066400000000000000000000032211476410620300272060ustar00rootroot00000000000000name: Build and Test Signer Darwin on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Build working-directory: ./internal/signer/darwin run: go build -v ./... # The binary must be built before creating credentials so it can be added # as a trusted application to the keychain. - name: Build Test Binary for Signer working-directory: ./internal/signer/darwin/keychain run: go test -c . - name: Create keychain credentials run: ./scripts/keychain_cred.sh - name: Run Test Binary for Signer working-directory: ./internal/signer/darwin/keychain run: ./keychain.test - name: Test Darwin Client working-directory: ./darwin run: go test -v . - name: Lint Signer uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: ./internal/signer/darwin args: -E gofmt --max-same-issues 0 - name: Lint Client uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: ./darwin args: -E gofmt --max-same-issues 0 - name: Create Binaries run: ./build/scripts/darwin_amd64.sh && ./build/scripts/darwin_arm64.sh - uses: actions/upload-artifact@v4 with: name: darwin_amd64 path: ./build/bin/darwin_amd64/* - uses: actions/upload-artifact@v4 with: name: darwin_arm64 path: ./build/bin/darwin_arm64/* enterprise-certificate-proxy-0.3.6/.github/workflows/test-signer-linux.yml000066400000000000000000000020711476410620300270630ustar00rootroot00000000000000name: Build and Test Signer Linux on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Bootstrap SoftHSM Credentials run: ./scripts/softhsm_setup.sh - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Build working-directory: ./internal/signer/linux run: go build -v ./... - name: Test working-directory: ./internal/signer/linux run: go test -v ./... -testSlot=$(pkcs11-tool --list-slots --module "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" | grep -Eo "0x[A-Fa-f0-9]+" | head -n 1) - name: Lint uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: ./internal/signer/linux args: -E gofmt --max-same-issues 0 - name: Create Binaries run: ./build/scripts/linux_amd64.sh - uses: actions/upload-artifact@v4 with: name: linux_amd64 path: ./build/bin/linux_amd64/* enterprise-certificate-proxy-0.3.6/.github/workflows/test-signer-windows.yml000066400000000000000000000015641476410620300274240ustar00rootroot00000000000000name: Build and Test Signer Windows on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Build working-directory: ./internal/signer/windows run: go build -v ./... - name: Test working-directory: ./internal/signer/windows run: go test -v ./... - name: Lint uses: golangci/golangci-lint-action@v6 with: version: latest working-directory: ./internal/signer/windows args: -E gofmt --max-same-issues 0 - name: Create Binaries run: .\build\scripts\windows_amd64.ps1 - uses: actions/upload-artifact@v4 with: name: windows_amd64 path: .\build\bin\windows_amd64\* enterprise-certificate-proxy-0.3.6/.gitignore000066400000000000000000000000641476410620300213320ustar00rootroot00000000000000# MacOS .DS_Store # compiled binaries build/bin/** enterprise-certificate-proxy-0.3.6/CODEOWNERS000066400000000000000000000001431476410620300207330ustar00rootroot00000000000000# Default owner for all directories not owned by others * @andyrzhao @clundin25 @ulisesL @yaoliugg enterprise-certificate-proxy-0.3.6/CODE_OF_CONDUCT.md000066400000000000000000000106271476410620300221470ustar00rootroot00000000000000# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when the Project Steward has a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Conflict Resolution We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s code of conduct. If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out to the Project Steward, please email opensource@google.com. We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html enterprise-certificate-proxy-0.3.6/CONTRIBUTING.md000066400000000000000000000021111476410620300215660ustar00rootroot00000000000000# How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code Reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). enterprise-certificate-proxy-0.3.6/LICENSE000066400000000000000000000261361476410620300203570ustar00rootroot00000000000000 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. enterprise-certificate-proxy-0.3.6/README.md000066400000000000000000000151771476410620300206340ustar00rootroot00000000000000# Google Proxies for Enterprise Certificates (GA) ## Certificate-based-access If you use [certificate-based access][cba] to protect your Google Cloud resources, the end user [device certificate][clientcert] is one of the credentials that is verified before access to a resource is granted. You can configure Google Cloud to use the device certificates in your operating system key store when verifying access to a resource from the gcloud CLI or Terraform by using the enterprise certificates feature. ## Google Enterprise Certificate Proxies (ECP) Google Enterprise Certificate Proxies (ECP) are part of the [Google Cloud Zero Trust architecture][zerotrust] that enables mutual authentication with [client-side certificates][clientcert]. This repository contains a set of proxies/modules that can be used by clients or toolings to interact with certificates that are stored in [protected key storage systems][keystore]. To interact the client certificates, application code should not need to use most of these proxies within this repository directly. Instead, the application should leverage the clients and toolings provided by Google such as [Cloud SDK](https://cloud.google.com/sdk) to have a more convenient developer experience. ## Compatibility The following platforms/keystores are supported by ECP: - MacOS: __Keychain__ - Linux: __PKCS#11__ - Windows: __MY__ ## User Guide Before using ECP with your application/client, you should complete the policy configurations documented in [Enable CBA for Enterprise Certificate][enterprisecert]. The remainder of this README focuses on client configuration. ### Quick Start 1. Install gcloud CLI (Cloud SDK) at: https://cloud.google.com/sdk/docs/install. Install with the bundled python option enabled. 1. **Note:** gcloud version 416.0 or newer is required. Version 430.0 or newer is recommended. 1. For macOS and Linux, run the install.sh script after downloading it to complete installation. ``` $ ./google-cloud-sdk/install.sh ``` 1. Install the ECP helper component: ``` $ gcloud components install enterprise-certificate-proxy ``` 1. Initialize ECP certificate configuration: * **MacOS** `$ gcloud auth enterprise-certificate-config create macos --issuer=` * **Linux** `$ gcloud auth enterprise-certificate-config create linux --label= --module= --slot=` * **Windows** `$ gcloud auth enterprise-certificate-config create windows --issuer= --provider= --store=` 1. Enable usage of client certificates through gcloud CLI config command: ``` $ gcloud config set context_aware/use_client_certificate true ``` 1. You can now use gcloud to access CBA-protected GCP resources. For example: ``` $ gcloud pubsub topics list ``` ### Manual Certificate Configuration ECP relies on a certificate configuration JSON file to read all the metadata information for locating the certificate. By default, it is named `certificate_config.json` and stored at the following location on the user's device: * **Linux and MacOS**: `~/.config/gcloud/certificate_config.json` * **Windows**: `%APPDATA%\gcloud\certificate_config.json` You can put the JSON file in the location of your choice and set the path to it using: ``` $ gcloud config set context_aware/certificate_config_file_path "" ``` Another approach for setting the JSON file location is with the `GOOGLE_API_CERTIFICATE_CONFIG` environment variable. ``` $ export GOOGLE_API_CERTIFICATE_CONFIG="" ``` Below are examples of the certificate configuration file: #### MacOS (Keychain) ```json { "cert_configs": { "macos_keychain": { "issuer": "YOUR_CERT_ISSUER" } }, "libs": { "ecp": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/bin/ecp", "ecp_client": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libecp.dylib", "tls_offload": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libtls_offload.dylib" }, "version": 1 } ``` #### Windows (MyStore) ```json { "cert_configs": { "windows_store": { "store": "MY", "provider": "current_user", "issuer": "YOUR_CERT_ISSUER" } }, "libs": { "ecp": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/bin/ecp.exe", "ecp_client": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libecp.dll", "tls_offload": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libtls_offload.dll" }, "version": 1 } ``` #### Linux (PKCS#11) ```json { "cert_configs": { "pkcs11": { "label": "YOUR_TOKEN_LABEL", "user_pin": "YOUR_PIN", "slot": "YOUR_SLOT", "module": "The PKCS #11 module library file path" } }, "libs": { "ecp": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/bin/ecp", "ecp_client": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libecp.so", "tls_offload": "[GCLOUD-INSTALL-LOCATION]/google-cloud-sdk/platform/enterprise_cert/libtls_offload.so" }, "version": 1 } ``` ### Logging To enable logging set the `ENABLE_ENTERPRISE_CERTIFICATE_LOGS` environment variable. #### Example ``` $ export ENABLE_ENTERPRISE_CERTIFICATE_LOGS=1 # Now the enterprise-certificate-proxy will output logs to stdout. ``` ## Building ECP binaries from source For amd64 MacOS, run `./build/scripts/darwin_amd64.sh`. The binaries will be placed in `build/bin/darwin_amd64` folder. For amd64 Linux, run `./build/scripts/linux_amd64.sh`. The binaries will be placed in `build/bin/linux_amd64` folder. For amd64 Windows, in powershell terminal, run `.\build\scripts\windows_amd64.ps1`. The binaries will be placed in `build\bin\windows_amd64` folder. Note that gcc is required for compiling the Windows shared library. The easiest way to get gcc on Windows is to download Mingw64, and add "gcc.exe" to the powershell path. ## Contributing Contributions to this library are always welcome and highly encouraged. See the [CONTRIBUTING](./CONTRIBUTING.md) documentation for more information on how to get started. ## License Apache - See [LICENSE](./LICENSE) for more information. [cba]: https://cloud.google.com/beyondcorp-enterprise/docs/securing-resources-with-certificate-based-access [clientcert]: https://en.wikipedia.org/wiki/Client_certificate [openssl]: https://wiki.openssl.org/index.php/Binaries [keystore]: https://en.wikipedia.org/wiki/Key_management [cloudsdk]: https://cloud.google.com/sdk [enterprisecert]: https://cloud.google.com/beyondcorp-enterprise/docs/enable-cba-enterprise-certificates [zerotrust]: https://cloud.google.com/blog/topics/developers-practitioners/zero-trust-and-beyondcorp-google-cloud enterprise-certificate-proxy-0.3.6/SECURITY.md000066400000000000000000000005111476410620300211300ustar00rootroot00000000000000# Security Policy To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). The Google Security Team will respond within 5 working days of your report on g.co/vulnz. We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. enterprise-certificate-proxy-0.3.6/build/000077500000000000000000000000001476410620300204415ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/build/scripts/000077500000000000000000000000001476410620300221305ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/build/scripts/darwin_amd64.sh000077500000000000000000000020541476410620300247470ustar00rootroot00000000000000#!/bin/bash # Copyright 2022 Google LLC. # 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 # # https://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. set -eux CURRENT_TAG=$(cat version.txt) # Create a folder to hold the binaries rm -rf ./build/bin/darwin_amd64 mkdir -p ./build/bin/darwin_amd64 # Build the signer binary cd ./internal/signer/darwin go build mv darwin ./../../../build/bin/darwin_amd64/ecp cd ./../../.. # Build the signer library go build -buildmode=c-shared -buildmode=c-shared -ldflags="-X=main.Version=$CURRENT_TAG" -o build/bin/darwin_amd64/libecp.dylib cshared/main.go rm build/bin/darwin_amd64/libecp.h enterprise-certificate-proxy-0.3.6/build/scripts/darwin_arm64.sh000077500000000000000000000021541476410620300247660ustar00rootroot00000000000000#!/bin/bash # Copyright 2022 Google LLC. # 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 # # https://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. set -eux CURRENT_TAG=$(cat version.txt) # Create a folder to hold the binaries rm -rf ./build/bin/darwin_arm64 mkdir -p ./build/bin/darwin_arm64 # Build the signer binary cd ./internal/signer/darwin CGO_ENABLED=1 GO111MODULE=on GOARCH=arm64 go build mv darwin ./../../../build/bin/darwin_arm64/ecp cd ./../../.. # Build the signer library CGO_ENABLED=1 GO111MODULE=on GOARCH=arm64 go build -buildmode=c-shared -ldflags="-X=main.Version=$CURRENT_TAG" -o build/bin/darwin_arm64/libecp.dylib cshared/main.go rm build/bin/darwin_arm64/libecp.h enterprise-certificate-proxy-0.3.6/build/scripts/linux_amd64.sh000077500000000000000000000020161476410620300246200ustar00rootroot00000000000000#!/bin/bash # Copyright 2022 Google LLC. # 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 # # https://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. set -eux CURRENT_TAG=$(cat version.txt) # Create a folder to hold the binaries rm -rf ./build/bin/linux_amd64 mkdir -p ./build/bin/linux_amd64 # Build the signer library go build -buildmode=c-shared -ldflags="-X=main.Version=$CURRENT_TAG" -o build/bin/linux_amd64/libecp.so cshared/main.go rm build/bin/linux_amd64/libecp.h # Build the signer binary cd ./internal/signer/linux go build mv linux ./../../../build/bin/linux_amd64/ecp cd ./../../.. enterprise-certificate-proxy-0.3.6/build/scripts/windows_amd64.ps1000066400000000000000000000025361476410620300252500ustar00rootroot00000000000000# Copyright 2022 Google LLC. # 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 # # https://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. $OutputFolder = ".\build\bin\windows_amd64" If (Test-Path $OutputFolder) { # Remove existing binaries Remove-Item -Path ".\build\bin\windows_amd64\*" } else { # Create the folder to hold the binaries New-Item -Path $OutputFolder -ItemType Directory -Force } # Build the signer binary Set-Location .\internal\signer\windows go build Move-Item .\windows.exe ..\..\..\build\bin\windows_amd64\ecp.exe Set-Location ..\..\..\ # Build the signer library # TODO: Add build version to shared DLL. https://github.com/googleapis/enterprise-certificate-proxy/issues/103 go build -buildmode=c-shared -o .\build\bin\windows_amd64\libecp.dll .\cshared\main.go go build -buildmode=c-archive -o .\build\bin\windows_amd64\libecp.lib .\cshared\main.go Remove-Item .\build\bin\windows_amd64\libecp.h enterprise-certificate-proxy-0.3.6/client/000077500000000000000000000000001476410620300206205ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/client/client.go000066400000000000000000000166201476410620300224320ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // Package client is a cross-platform client for the signer binary (a.k.a."EnterpriseCertSigner"). // // The signer binary is OS-specific, but exposes a standard set of APIs for the client to use. package client import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/gob" "errors" "fmt" "io" "net/rpc" "os" "os/exec" "github.com/googleapis/enterprise-certificate-proxy/client/util" ) const signAPI = "EnterpriseCertSigner.Sign" const certificateChainAPI = "EnterpriseCertSigner.CertificateChain" const publicKeyAPI = "EnterpriseCertSigner.Public" const encryptAPI = "EnterpriseCertSigner.Encrypt" const decryptAPI = "EnterpriseCertSigner.Decrypt" // A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. type Connection struct { io.ReadCloser io.WriteCloser } // Close closes c's underlying ReadCloser and WriteCloser. func (c *Connection) Close() error { rerr := c.ReadCloser.Close() werr := c.WriteCloser.Close() if rerr != nil { return rerr } return werr } func init() { gob.Register(crypto.SHA256) gob.Register(crypto.SHA384) gob.Register(crypto.SHA512) gob.Register(&rsa.PSSOptions{}) gob.Register(&rsa.OAEPOptions{}) } // SignArgs contains arguments for a Sign API call. type SignArgs struct { Digest []byte // The content to sign. Opts crypto.SignerOpts // Options for signing. Must implement HashFunc(). } // EncryptArgs contains arguments for an Encrypt API call. type EncryptArgs struct { Plaintext []byte // The plaintext to encrypt. Opts any // Options for encryption. Ex: an instance of crypto.Hash. } // DecryptArgs contains arguments to for a Decrypt API call. type DecryptArgs struct { Ciphertext []byte // The ciphertext to decrypt. Opts crypto.DecrypterOpts // Options for decryption. Ex: an instance of *rsa.OAEPOptions. } // Key implements credential.Credential by holding the executed signer subprocess. type Key struct { cmd *exec.Cmd // Pointer to the signer subprocess. client *rpc.Client // Pointer to the rpc client that communicates with the signer subprocess. publicKey crypto.PublicKey // Public key of loaded certificate. chain [][]byte // Certificate chain of loaded certificate. } // CertificateChain returns the credential as a raw X509 cert chain. This contains the public key. func (k *Key) CertificateChain() [][]byte { return k.chain } // Close closes the RPC connection and kills the signer subprocess. // Call this to free up resources when the Key object is no longer needed. func (k *Key) Close() error { if err := k.cmd.Process.Kill(); err != nil { return fmt.Errorf("failed to kill signer process: %w", err) } // Wait for cmd to exit and release resources. Since the process is forcefully killed, this // will return a non-nil error (varies by OS), which we will ignore. _ = k.cmd.Wait() // The Pipes connecting the RPC client should have been closed when the signer subprocess was killed. // Calling `k.client.Close()` before `k.cmd.Process.Kill()` or `k.cmd.Wait()` _will_ cause a segfault. if err := k.client.Close(); err.Error() != "close |0: file already closed" { return fmt.Errorf("failed to close RPC connection: %w", err) } return nil } // Public returns the public key for this Key. func (k *Key) Public() crypto.PublicKey { return k.publicKey } // Sign signs a message digest, using the specified signer opts. Implements crypto.Signer interface. func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signed []byte, err error) { if opts != nil && opts.HashFunc() != 0 && len(digest) != opts.HashFunc().Size() { return nil, fmt.Errorf("Digest length of %v bytes does not match Hash function size of %v bytes", len(digest), opts.HashFunc().Size()) } err = k.client.Call(signAPI, SignArgs{Digest: digest, Opts: opts}, &signed) return } // Encrypt encrypts a plaintext msg into ciphertext, using the specified encrypt opts. func (k *Key) Encrypt(_ io.Reader, msg []byte, opts any) (ciphertext []byte, err error) { err = k.client.Call(encryptAPI, EncryptArgs{Plaintext: msg, Opts: opts}, &ciphertext) return } // Decrypt decrypts a ciphertext msg into plaintext, using the specified decrypter opts. Implements crypto.Decrypter interface. func (k *Key) Decrypt(_ io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { err = k.client.Call(decryptAPI, DecryptArgs{Ciphertext: msg, Opts: opts}, &plaintext) return } // ErrCredUnavailable is a sentinel error that indicates ECP Cred is unavailable, // possibly due to missing config or missing binary path. var ErrCredUnavailable = errors.New("Cred is unavailable") // Cred spawns a signer subprocess that listens on stdin/stdout to perform certificate // related operations, including signing messages with the private key. // // The signer binary path is read from the specified configFilePath, if provided. // Otherwise, use the default config file path. // // The config file also specifies which certificate the signer should use. func Cred(configFilePath string) (*Key, error) { if configFilePath == "" { envFilePath := util.GetConfigFilePathFromEnv() if envFilePath != "" { configFilePath = envFilePath } else { configFilePath = util.GetDefaultConfigFilePath() } } enterpriseCertSignerPath, err := util.LoadSignerBinaryPath(configFilePath) if err != nil { if errors.Is(err, util.ErrConfigUnavailable) { return nil, ErrCredUnavailable } return nil, err } k := &Key{ cmd: exec.Command(enterpriseCertSignerPath, configFilePath), } // Redirect errors from subprocess to parent process. k.cmd.Stderr = os.Stderr // RPC client will communicate with subprocess over stdin/stdout. kin, err := k.cmd.StdinPipe() if err != nil { return nil, err } kout, err := k.cmd.StdoutPipe() if err != nil { return nil, err } k.client = rpc.NewClient(&Connection{kout, kin}) if err := k.cmd.Start(); err != nil { return nil, fmt.Errorf("starting enterprise cert signer subprocess: %w", err) } if err := k.client.Call(certificateChainAPI, struct{}{}, &k.chain); err != nil { return nil, fmt.Errorf("failed to retrieve certificate chain: %w", err) } var publicKeyBytes []byte if err := k.client.Call(publicKeyAPI, struct{}{}, &publicKeyBytes); err != nil { return nil, fmt.Errorf("failed to retrieve public key: %w", err) } publicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes) if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } var ok bool k.publicKey, ok = publicKey.(crypto.PublicKey) if !ok { return nil, fmt.Errorf("invalid public key type: %T", publicKey) } switch pub := k.publicKey.(type) { case *rsa.PublicKey: if pub.Size() < 256 { return nil, fmt.Errorf("RSA modulus size is less than 2048 bits: %v", pub.Size()*8) } case *ecdsa.PublicKey: default: return nil, fmt.Errorf("unsupported public key type: %v", pub) } return k, nil } enterprise-certificate-proxy-0.3.6/client/client_test.go000066400000000000000000000105501476410620300234650ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // The tests in this file launches a mock signer binary "signer.go". package client import ( "bytes" "crypto" "crypto/rsa" "encoding/json" "errors" "os" "testing" ) func TestClient_Cred_Success(t *testing.T) { _, err := Cred("testdata/certificate_config.json") if err != nil { t.Errorf("Cred: got %v, want nil err", err) } } func TestClient_Cred_ConfigMissing(t *testing.T) { _, err := Cred("missing.json") if got, want := err, ErrCredUnavailable; !errors.Is(got, want) { t.Errorf("Cred: with missing config; got %v, want %v err", got, want) } } func TestClient_Cred_BinaryPathMissing(t *testing.T) { _, err := Cred("testdata/certificate_config_missing_path.json") if got, want := err, ErrCredUnavailable; !errors.Is(got, want) { t.Errorf("Cred: with missing ECP path; got %v, want %v err", got, want) } } func TestClient_Cred_EnvOverride_ExplicitConfig(t *testing.T) { configFilePath := "testdata/certificate_config.json" os.Setenv("GOOGLE_API_CERTIFICATE_CONFIG", "testdata/certificate_config_missing_path.json") _, err := Cred(configFilePath) if err != nil { t.Errorf("Cred: with explicit config and set env var; got %v, want %v err", err, nil) } } func TestClient_Cred_EnvOverride_EmptyConfig(t *testing.T) { configFilePath := "" os.Setenv("GOOGLE_API_CERTIFICATE_CONFIG", "testdata/certificate_config_broken.json") _, err := Cred(configFilePath) var serr *json.SyntaxError if got, want := err, &serr; !errors.As(got, want) { t.Errorf("Cred: with empty config and set env var; got %v, want %v err", got, want) } } func TestClient_Public(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } if key.Public() == nil { t.Error("Public: got nil, want non-nil Public Key") } } func TestClient_CertificateChain(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } if key.CertificateChain() == nil { t.Error("CertificateChain: got nil, want non-nil Certificate Chain") } } func TestClient_Sign(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } signed, err := key.Sign(nil, []byte("testDigest"), nil) if err != nil { t.Fatal(err) } if got, want := signed, []byte("testDigest"); !bytes.Equal(got, want) { t.Errorf("Sign: got %c, want %c", got, want) } } func TestClientEncrypt(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } plaintext := []byte("Plain text to encrypt") _, err = key.Encrypt(nil, plaintext, crypto.SHA256) if err != nil { t.Errorf("Universal Client API encryption: got %v, want nil err", err) return } } func TestClientDecrypt(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } byteSlice := []byte("Plain text to encrypt") ciphertext, _ := key.Encrypt(nil, byteSlice, crypto.SHA256) plaintext, err := key.Decrypt(nil, ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA256}) if err != nil { t.Errorf("Universal Client API decryption: got %v, want nil err", err) return } if !bytes.Equal(byteSlice, plaintext) { t.Errorf("Decryption message does not match original: got %v, want %v", plaintext, byteSlice) } } func TestClient_Sign_HashSizeMismatch(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } _, err = key.Sign(nil, []byte("testDigest"), crypto.SHA256) if got, want := err.Error(), "Digest length of 10 bytes does not match Hash function size of 32 bytes"; got != want { t.Errorf("Sign: got err %v, want err %v", got, want) } } func TestClient_Close(t *testing.T) { key, err := Cred("testdata/certificate_config.json") if err != nil { t.Fatal(err) } err = key.Close() if err != nil { t.Errorf("Close: got %v, want nil err", err) } } enterprise-certificate-proxy-0.3.6/client/testdata/000077500000000000000000000000001476410620300224315ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/client/testdata/certificate_config.json000066400000000000000000000002011476410620300271240ustar00rootroot00000000000000{ "cert_configs": { "test": { "issuer": "Test Issuer" } }, "libs": { "ecp": "./testdata/signer.sh" } } enterprise-certificate-proxy-0.3.6/client/testdata/certificate_config_broken.json000066400000000000000000000000301476410620300304640ustar00rootroot00000000000000{ "cert_configs": { } enterprise-certificate-proxy-0.3.6/client/testdata/certificate_config_missing_path.json000066400000000000000000000001371476410620300317010ustar00rootroot00000000000000{ "cert_configs": { "test": { "issuer": "Test Issuer" } }, "libs": { } } enterprise-certificate-proxy-0.3.6/client/testdata/signer.sh000077500000000000000000000012141476410620300242550ustar00rootroot00000000000000#!/bin/bash # Copyright 2022 Google LLC. # 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 # # https://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. go run ../internal/signer/test/signer.go testdata/testcert.pementerprise-certificate-proxy-0.3.6/client/testdata/testcert.pem000066400000000000000000000056011476410620300247730ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDZjCCAk4CCQCN7UdavjYDjjANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJV UzELMAkGA1UECAwCV0ExDzANBgNVBAcMBlJlbnRvbjEMMAoGA1UECgwDQ0JBMQww CgYDVQQLDANFQ1AxDTALBgNVBAMMBHRlc3QxHTAbBgkqhkiG9w0BCQEWDnRlc3RA Z21haWwuY29tMB4XDTIyMDkxMTE2MzIwMVoXDTMyMDkwODE2MzIwMVowdTELMAkG A1UEBhMCVVMxCzAJBgNVBAgMAldBMQ8wDQYDVQQHDAZSZW50b24xDDAKBgNVBAoM A0NCQTEMMAoGA1UECwwDRUNQMQ0wCwYDVQQDDAR0ZXN0MR0wGwYJKoZIhvcNAQkB Fg50ZXN0QGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB APlRXo2ji8rFYfF8ew7Fsi3KuHMvirW1/OGhhPaqGGDomvFpoAwf5MQn4RIOFzf0 KCy3bRSHjMJlRfINf/FgByjLik8NRcI3huHlDyAZS4Va4b/L4GIfA7jPuIu/HsAu eGIOOncBpyKyRwaf2HhGAvy85MfWAvHr+3k0gL90nGQWFjvRDt+wyLLUZ5SIMDUT x7aBji9qGAxX2sbiFB0C7chK4mwsPKowgK+fIgHkbqSIN6IyFIU6pLXGKJ1WrNBg CHA2LPUE477GKinuaDq4PjQyVQF9MAQmK4hRu8N7COJeZunHWQJjACT5QRxmMiWp H2dYbX6Wg3eXMRpbGVoLuHUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFvfK3t5u tK3+PPhkpCoEpcequn5vTOKDBSE95o3Od/RmNQEmUqSsuPtBd5ZVxKKa+ZapVowt S9YFr5C9jgUleukLEYQNj0p8jrcZjVaUy5hmDynaIlkbtl5NHGyNOeJMJprA5ylV wQ3ULnGjIxx3AsCEYeSp+eea6jztl5cvH6nGj6rI20lhrrHfKjxaGCRT+4X7NcXP jSQrvaQjZKjs20iVX1f/En4OgR4FY5YJkMRhrebcoYnldkKzWjNpy3j/QwVWNzl3 1jfpeDUw8o7a4UDONMIwQjMQq05tqTh9WbL+6B2CEQnhPeKAGm8oHwyqdux8A9Nf Lw4UcyjbQOSWlA== -----END CERTIFICATE----- -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD5UV6No4vKxWHx fHsOxbItyrhzL4q1tfzhoYT2qhhg6JrxaaAMH+TEJ+ESDhc39Cgst20Uh4zCZUXy DX/xYAcoy4pPDUXCN4bh5Q8gGUuFWuG/y+BiHwO4z7iLvx7ALnhiDjp3AaciskcG n9h4RgL8vOTH1gLx6/t5NIC/dJxkFhY70Q7fsMiy1GeUiDA1E8e2gY4vahgMV9rG 4hQdAu3ISuJsLDyqMICvnyIB5G6kiDeiMhSFOqS1xiidVqzQYAhwNiz1BOO+xiop 7mg6uD40MlUBfTAEJiuIUbvDewjiXmbpx1kCYwAk+UEcZjIlqR9nWG1+loN3lzEa WxlaC7h1AgMBAAECggEBANhlYs9HO4d1CMzkQZ8RwtRyFuSLSDbtzZ89ZT3/Zwd9 /TY6eprrd9E11+mm50o+ljwxvPDLskXsRuiQBRPJSI2FFPgGSh0HuwAIo7c1nVIT Dsw9NfWUe9OGH+TTruoZq41YUjCG871uxa0fQnEqO1+IyH4W6Bl4vJ14D6OdoDxR JMzZyeddezQlyCS+Mi/jMi82YZWdCdhr1mpTtzVpWJEvKj0VUQFSd4ioS87/F7la RT42Y1t6igfvHjnVV0w0mf+32UiLlDkOZ/xY215/9aYfJMhak7ctUGx3BgJgoyDP hlRQxS2dzzgZGQrgjI+7jnyIbvGbhS2o6j8JorsTE6ECgYEA/aWK7iBtj1lH3O0t 6atOb/yT76k9bHaFgX0gU+H/8bxiGt0V2r4+IoAWtwJNgi3As9jP4yJPiCiRX2PW yIhRZkEkoZ4uPSabPLtoKd/95sytiIQ57KRQGhUYehVz1Uockt4c6FfDi0XPkFek /9N9Fv/sJxhWHp0hMn0u0oBVd2kCgYEA+6GLghm6roxQdq2kjAsBKHXmr1emfuzQ BvucM3t8wh04I4r3jc2GpmCI428dtHQkYRTV5bdWrxI1MeIWxzumW1hXzjkuV+fI WDX9gLCOB3d7mtHmwXunSHpwvZygXRZH3y4xYmTOpQgZAIxm1Gm6FsvMFVExF+UD m06QWH0zgy0CgYBSp1s6db69864DRBauCnCo9XmPo2qsqYKfy5J5QzAQKf8eGeVB PrUosOy1/j4bqaUd9gzoSwn3qKCWoQYgmqtL0vaI4+7VZns3syoiWyd1ykTSM6Rc hL7FgRJU1iDE5D2jblWlMNQ70iftNWJDKzub/xGJO9j0aOekeD6FweQX4QKBgHwe 0FjpZhtJXTtdJchqeTTDC3o8SwVavLZlEESYyg5aKWHm33uUALI69erx2X40t+kn ROceC2UqHxEvC7tU4hc2uYEg1YpI65sPbq8256gpONBCb4fK/dYTh18QTk38epFN ENEPFptzJhoOJ37pdABgoJd3SDcYITJPi4YKpAk1AoGBAOaQN50lZQuIAab4hwmd hEXpn2YA8+qU8K1Y4DdJNBdKt+JDzN30+B7qZ1vVvyDCCIEhoKAr9b/wjQHF2pC4 Vp89uLNKTLF6Pg4Wm+71MbDPFFRTyMghOPBn3vWQvj81sLMselg8eIjTO8XS7mQ7 hPJVfKseNBDOBE4OolLNAoBK -----END PRIVATE KEY----- enterprise-certificate-proxy-0.3.6/client/util/000077500000000000000000000000001476410620300215755ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/client/util/test_data/000077500000000000000000000000001476410620300235455ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/client/util/test_data/certificate_config.json000066400000000000000000000001411476410620300302430ustar00rootroot00000000000000{ "libs": { "ecp": "C:/Program Files (x86)/Google/Endpoint Verification/signer.exe" } } enterprise-certificate-proxy-0.3.6/client/util/test_data/certificate_config_home_expansion.json000066400000000000000000000000671476410620300333460ustar00rootroot00000000000000{ "libs": { "ecp": "$HOME/ecp/signer" } }enterprise-certificate-proxy-0.3.6/client/util/test_data/certificate_config_tilde_expansion.json000066400000000000000000000000631476410620300335130ustar00rootroot00000000000000{ "libs": { "ecp": "~/ecp/signer" } }enterprise-certificate-proxy-0.3.6/client/util/util.go000066400000000000000000000057041476410620300231070ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // Package util provides helper functions for the client. package util import ( "encoding/json" "errors" "io" "os" "os/user" "path/filepath" "runtime" "strings" ) const configFileName = "certificate_config.json" // EnterpriseCertificateConfig contains parameters for initializing signer. type EnterpriseCertificateConfig struct { Libs Libs `json:"libs"` } // Libs specifies the locations of helper libraries. type Libs struct { ECP string `json:"ecp"` } // ErrConfigUnavailable is a sentinel error that indicates ECP config is unavailable, // possibly due to entire config missing or missing binary path. var ErrConfigUnavailable = errors.New("Config is unavailable") // LoadSignerBinaryPath retrieves the path of the signer binary from the config file. func LoadSignerBinaryPath(configFilePath string) (path string, err error) { jsonFile, err := os.Open(configFilePath) if err != nil { if errors.Is(err, os.ErrNotExist) { return "", ErrConfigUnavailable } return "", err } byteValue, err := io.ReadAll(jsonFile) if err != nil { return "", err } var config EnterpriseCertificateConfig err = json.Unmarshal(byteValue, &config) if err != nil { return "", err } signerBinaryPath := config.Libs.ECP if signerBinaryPath == "" { return "", ErrConfigUnavailable } signerBinaryPath = strings.ReplaceAll(signerBinaryPath, "~", guessHomeDir()) signerBinaryPath = strings.ReplaceAll(signerBinaryPath, "$HOME", guessHomeDir()) return signerBinaryPath, nil } func guessHomeDir() string { // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 if v := os.Getenv("HOME"); v != "" { return v } // Else, fall back to user.Current: if u, err := user.Current(); err == nil { return u.HomeDir } return "" } func getDefaultConfigFileDirectory() (directory string) { if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("APPDATA"), "gcloud") } return filepath.Join(guessHomeDir(), ".config/gcloud") } // GetDefaultConfigFilePath returns the default path of the enterprise certificate config file created by gCloud. func GetDefaultConfigFilePath() (path string) { return filepath.Join(getDefaultConfigFileDirectory(), configFileName) } // GetConfigFilePathFromEnv returns the path associated with environment variable GOOGLE_API_CERTIFICATE_CONFIG func GetConfigFilePathFromEnv() (path string) { return os.Getenv("GOOGLE_API_CERTIFICATE_CONFIG") } enterprise-certificate-proxy-0.3.6/client/util/util_test.go000066400000000000000000000035161476410620300241450ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. package util import ( "os" "testing" ) func TestLoadSignerBinaryPath(t *testing.T) { path, err := LoadSignerBinaryPath("./test_data/certificate_config.json") if err != nil { t.Errorf("LoadSignerBinaryPath error: %q", err) } want := "C:/Program Files (x86)/Google/Endpoint Verification/signer.exe" if path != want { t.Errorf("Expected path is %q, got: %q", want, path) } } func TestLoadSignerBinaryPathHome(t *testing.T) { path, err := LoadSignerBinaryPath("./test_data/certificate_config_home_expansion.json") if err != nil { t.Errorf("LoadSignerBinaryPath error: %q", err) } want := guessHomeDir() + "/ecp/signer" if path != want { t.Errorf("Expected path is %q, got: %q", want, path) } } func TestLoadSignerBinaryPathTilde(t *testing.T) { path, err := LoadSignerBinaryPath("./test_data/certificate_config_tilde_expansion.json") if err != nil { t.Errorf("LoadSignerBinaryPath error: %q", err) } want := guessHomeDir() + "/ecp/signer" if path != want { t.Errorf("Expected path is %q, got: %q", want, path) } } func TestGetConfigFilePathFromEnv(t *testing.T) { want := "/testpath" os.Setenv("GOOGLE_API_CERTIFICATE_CONFIG", want) path := GetConfigFilePathFromEnv() if path != want { t.Errorf("Expected path is %q, got: %q", want, path) } } enterprise-certificate-proxy-0.3.6/cshared/000077500000000000000000000000001476410620300207535ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/cshared/main.go000066400000000000000000000147751476410620300222440ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // This package is intended to be compiled into a C shared library for // use by non-Golang clients to perform certificate and signing operations. // // The shared library exports language-specific wrappers around the Golang // client APIs. // // Example compilation command: // go build -buildmode=c-shared -o signer.dylib main.go package main /* #include */ import "C" import ( "crypto" "crypto/ecdsa" "crypto/rsa" "encoding/pem" "io" "log" "os" "unsafe" "github.com/googleapis/enterprise-certificate-proxy/client" ) // Version is generally set by the build command. Releases of ECP must have a specific version set. // The version can be set when running `go build` like so `-ldflags="-X=main.Version=$CURRENT_TAG" `. var Version = "dev" // If ECP Logging is enabled return true // Otherwise return false func enableECPLogging() bool { if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { return true } log.SetOutput(io.Discard) return false } func getCertPem(configFilePath string) []byte { key, err := client.Cred(configFilePath) if err != nil { log.Printf("Could not create client using config %s: %v", configFilePath, err) return nil } defer func() { if err = key.Close(); err != nil { log.Printf("Failed to clean up key. %v", err) } }() certChain := key.CertificateChain() certChainPem := []byte{} for i := 0; i < len(certChain); i++ { certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certChain[i]}) certChainPem = append(certChainPem, certPem...) } return certChainPem } // ECPVersion returns ECP's version number. // //export ECPVersion func ECPVersion() *C.char { return C.CString(Version) } // GetCertPem reads the contents of the certificate specified by configFilePath, // storing the result inside a certHolder byte array of size certHolderLen. // // We must call it twice to get the cert. First time use nil for certHolder to get // the cert length. Second time we pre-create an array of the cert length and // call this function again to load the cert into the array. // //export GetCertPem func GetCertPem(configFilePath *C.char, certHolder *byte, certHolderLen int) int { enableECPLogging() pemBytes := getCertPem(C.GoString(configFilePath)) if certHolder != nil { cert := unsafe.Slice(certHolder, certHolderLen) copy(cert, pemBytes) } return len(pemBytes) } // GetCertPemForPython reads the contents of the certificate specified by configFilePath, // storing the result inside a certHolder byte array of size certHolderLen. // // We must call it twice to get the cert. First time use nil for certHolder to get // the cert length. Second time we pre-create an array in Python of the cert length and // call this function again to load the cert into the array. // // Deprecated: This API is deprecated in favor of GetCertPem and will be removed in future versions. // //export GetCertPemForPython func GetCertPemForPython(configFilePath *C.char, certHolder *byte, certHolderLen int) int { return GetCertPem(configFilePath, certHolder, certHolderLen) } // Sign signs a message digest of length digestLen using a certificate private key // specified by configFilePath, storing the result inside a sigHolder byte array of size sigHolderLen. // //export Sign func Sign(configFilePath *C.char, digest *byte, digestLen int, sigHolder *byte, sigHolderLen int) int { // First create a handle around the specified certificate and private key. enableECPLogging() key, err := client.Cred(C.GoString(configFilePath)) if err != nil { log.Printf("Could not create client using config %s: %v", C.GoString(configFilePath), err) return 0 } defer func() { if err = key.Close(); err != nil { log.Printf("Failed to clean up key. %v", err) } }() var isRsa bool switch key.Public().(type) { case *ecdsa.PublicKey: isRsa = false log.Print("the key is ecdsa key") case *rsa.PublicKey: isRsa = true log.Print("the key is rsa key") default: log.Printf("unsupported key type") return 0 } // Compute the signature digestSlice := unsafe.Slice(digest, digestLen) var signature []byte var signErr error if isRsa { // For RSA key, we need to create the padding and flags for RSASSA-SHA256 opts := rsa.PSSOptions{ SaltLength: digestLen, Hash: crypto.SHA256, } signature, signErr = key.Sign(nil, digestSlice, &opts) } else { signature, signErr = key.Sign(nil, digestSlice, crypto.SHA256) } if signErr != nil { log.Printf("failed to sign hash: %v", signErr) return 0 } if sigHolderLen < len(signature) { log.Printf("The sigHolder buffer size %d is smaller than the signature size %d", sigHolderLen, len(signature)) return 0 } // Create a Go buffer around the output buffer and copy the signature into the buffer outBytes := unsafe.Slice(sigHolder, sigHolderLen) copy(outBytes, signature) return len(signature) } // SignForPython signs a message digest of length digestLen using a certificate private key // specified by configFilePath, storing the result inside a sigHolder byte array of size sigHolderLen. // // Deprecated: This API is deprecated in favor of Sign and will be removed in future versions. // //export SignForPython func SignForPython(configFilePath *C.char, digest *byte, digestLen int, sigHolder *byte, sigHolderLen int) int { return Sign(configFilePath, digest, digestLen, sigHolder, sigHolderLen) } // GetKeyType returns a string representing ECP's key type. // The key is derived from the ECP configuration. // //export GetKeyType func GetKeyType(configFilePath *C.char) *C.char { key, err := client.Cred(C.GoString(configFilePath)) if err != nil { log.Printf("Could not create client using config %s: %v", C.GoString(configFilePath), err) return C.CString("unknown") } defer func() { if err = key.Close(); err != nil { log.Printf("Failed to clean up key. %v", err) } }() switch key.Public().(type) { case *ecdsa.PublicKey: return C.CString("EC") case *rsa.PublicKey: return C.CString("RSA") default: return C.CString("unknown") } } func main() {} enterprise-certificate-proxy-0.3.6/darwin/000077500000000000000000000000001476410620300206265ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/darwin/client.go000066400000000000000000000064411476410620300224400ustar00rootroot00000000000000// Copyright 2023 Google LLC. // 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 // // https://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. //go:build darwin && cgo // +build darwin,cgo // Package darwin contains a darwin-specific client for accessing the keychain APIs directly, // bypassing the RPC mechanism of the universal client. package darwin import ( "crypto" "io" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/darwin/keychain" ) // SecureKey is a public wrapper for the internal keychain implementation. type SecureKey struct { key *keychain.Key } // SecureKeyOptions encapsulates all supported keychain parameters type SecureKeyOptions struct { IssuerCN string KeychainType string } // CertificateChain returns the SecureKey's raw X509 cert chain. This contains the public key. func (sk *SecureKey) CertificateChain() [][]byte { return sk.key.CertificateChain() } // Public returns the public key for this SecureKey. func (sk *SecureKey) Public() crypto.PublicKey { return sk.key.Public() } // Sign signs a message digest, using the specified signer opts. Implements crypto.Signer interface. func (sk *SecureKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signed []byte, err error) { return sk.key.Sign(nil, digest, opts) } // Encrypt encrypts a plaintext msg into ciphertext, using the specified encrypt opts. func (sk *SecureKey) Encrypt(_ io.Reader, msg []byte, opts any) (ciphertext []byte, err error) { return sk.key.Encrypt(msg, opts) } // Decrypt decrypts a ciphertext msg into plaintext, using the specified decrypter opts. Implements crypto.Decrypter interface. func (sk *SecureKey) Decrypt(_ io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { return sk.key.Decrypt(msg, opts) } // Close frees up resources associated with the underlying key. func (sk *SecureKey) Close() { sk.key.Close() } // NewSecureKey returns a handle to the first available certificate and private key pair in // the MacOS Keychain matching the issuer CN filter. This includes both the current login keychain // for the user as well as the system keychain. func NewSecureKey(issuerCN string) (*SecureKey, error) { k, err := keychain.Cred(issuerCN, "") if err != nil { return nil, err } return &SecureKey{key: k}, nil } // NewSecureKeyWithOptions returns a handle to the first available certificate and private key pair in // the MacOS Keychain matching the SecureKeyOptions filter. func NewSecureKeyWithOptions(options SecureKeyOptions) (*SecureKey, error) { k, err := keychain.Cred(options.IssuerCN, options.KeychainType) if err != nil { return nil, err } return &SecureKey{key: k}, nil } // ImportPKCS12Cred imports a PKCS12 file containing a client certificate and private key into the keychain func ImportPKCS12Cred(credPath, password string) error { return keychain.ImportPKCS12Cred(credPath, password) } enterprise-certificate-proxy-0.3.6/darwin/client_test.go000066400000000000000000000027501476410620300234760ustar00rootroot00000000000000// Copyright 2023 Google LLC. // 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 // // https://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. package darwin import ( "crypto" "testing" ) const testIssuer = "TestIssuer" func TestNewSecureKeyWithOptions(t *testing.T) { opts := SecureKeyOptions{ IssuerCN: testIssuer, KeychainType: "all", } _, err := NewSecureKeyWithOptions(opts) if err != nil { t.Errorf("Cred: got %v, want nil err", err) return } } func TestClientEncrypt(t *testing.T) { secureKey, err := NewSecureKey(testIssuer) if err != nil { t.Errorf("Cred: got %v, want nil err", err) return } plaintext := []byte("Plain text to encrypt") _, err = secureKey.Encrypt(nil, plaintext, crypto.SHA256) if err != nil { t.Errorf("Client API encryption: got %v, want nil err", err) return } } func TestImportPKCS12Cred(t *testing.T) { credPath := "../testdata/testcred.p12" password := "1234" err := ImportPKCS12Cred(credPath, password) if err != nil { t.Errorf("ImportPKCS12Cred: got %v, want nil err", err) return } } enterprise-certificate-proxy-0.3.6/go.mod000066400000000000000000000003011476410620300204420ustar00rootroot00000000000000module github.com/googleapis/enterprise-certificate-proxy go 1.23.0 toolchain go1.23.1 require ( github.com/google/go-pkcs11 v0.3.0 golang.org/x/crypto v0.35.0 golang.org/x/sys v0.30.0 ) enterprise-certificate-proxy-0.3.6/go.sum000066400000000000000000000007451476410620300205030ustar00rootroot00000000000000github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= enterprise-certificate-proxy-0.3.6/internal/000077500000000000000000000000001476410620300211565ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/000077500000000000000000000000001476410620300224455ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/darwin/000077500000000000000000000000001476410620300237315ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/darwin/keychain/000077500000000000000000000000001476410620300255245ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/darwin/keychain/keychain.go000066400000000000000000000655301476410620300276570ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. //go:build darwin && cgo // +build darwin,cgo // Package keychain contains functions for retrieving certificates from the Darwin Keychain. package keychain /* #cgo CFLAGS: -mmacosx-version-min=10.12 #cgo LDFLAGS: -framework CoreFoundation -framework Security #include #include */ import "C" import ( "bytes" "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io" "os" "os/user" "path/filepath" "runtime" "sync" "time" "unsafe" ) // Maps for translating from crypto.Hash to SecKeyAlgorithm. // https://developer.apple.com/documentation/security/seckeyalgorithm var ( ecdsaAlgorithms = map[crypto.Hash]C.CFStringRef{ crypto.SHA256: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA256, crypto.SHA384: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA384, crypto.SHA512: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA512, } rsaRaw = map[crypto.Hash]C.CFStringRef{ crypto.SHA256: C.kSecKeyAlgorithmRSAEncryptionRaw, } rsaPKCS1v15Algorithms = map[crypto.Hash]C.CFStringRef{ crypto.SHA256: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256, crypto.SHA384: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384, crypto.SHA512: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512, } rsaPSSAlgorithms = map[crypto.Hash]C.CFStringRef{ crypto.SHA256: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256, crypto.SHA384: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384, crypto.SHA512: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512, } rsaOAEPAlgorithms = map[crypto.Hash]C.CFStringRef{ crypto.SHA256: C.kSecKeyAlgorithmRSAEncryptionOAEPSHA256, crypto.SHA384: C.kSecKeyAlgorithmRSAEncryptionOAEPSHA384, crypto.SHA512: C.kSecKeyAlgorithmRSAEncryptionOAEPSHA512, } ) const unknownSecKeyAlgorithm = C.CFStringRef(0) const invalidKey = C.SecKeyRef(0) // cfStringToString returns a Go string given a CFString. func cfStringToString(cfStr C.CFStringRef) string { s := C.CFStringGetCStringPtr(cfStr, C.kCFStringEncodingUTF8) if s != nil { return C.GoString(s) } glyphLength := C.CFStringGetLength(cfStr) + 1 utf8Length := C.CFStringGetMaximumSizeForEncoding(glyphLength, C.kCFStringEncodingUTF8) if s = (*C.char)(C.malloc(C.size_t(utf8Length))); s == nil { panic("unable to allocate memory") } defer C.free(unsafe.Pointer(s)) if C.CFStringGetCString(cfStr, s, utf8Length, C.kCFStringEncodingUTF8) == 0 { panic("unable to convert cfStringref to string") } return C.GoString(s) } func cfRelease(x unsafe.Pointer) { C.CFRelease(C.CFTypeRef(x)) } // cfError is an error type that owns a CFErrorRef, and obtains the error string // by using CFErrorCopyDescription. type cfError struct { e C.CFErrorRef } // cfErrorFromRef converts a C.CFErrorRef to a cfError, taking ownership of the // reference and releasing when the value is finalized. func cfErrorFromRef(cfErr C.CFErrorRef) error { if cfErr == 0 { return nil } c := &cfError{e: cfErr} runtime.SetFinalizer(c, func(x interface{}) { C.CFRelease(C.CFTypeRef(x.(*cfError).e)) }) return c } func (e *cfError) Error() string { s := C.CFErrorCopyDescription(C.CFErrorRef(e.e)) defer C.CFRelease(C.CFTypeRef(s)) return cfStringToString(s) } // keychainError is an error type that is based on an OSStatus return code, and // obtains the error string with SecCopyErrorMessageString. type keychainError C.OSStatus func (e keychainError) Error() string { s := C.SecCopyErrorMessageString(C.OSStatus(e), nil) defer C.CFRelease(C.CFTypeRef(s)) return cfStringToString(s) } // cfDataToBytes turns a CFDataRef into a byte slice. func cfDataToBytes(cfData C.CFDataRef) []byte { return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))) } // bytesToCFData turns a byte slice into a CFDataRef. Caller then "owns" the // CFDataRef and must CFRelease the CFDataRef when done. func bytesToCFData(buf []byte) C.CFDataRef { return C.CFDataCreate(C.kCFAllocatorDefault, (*C.UInt8)(unsafe.Pointer(&buf[0])), C.CFIndex(len(buf))) } // int32ToCFNumber turns an int32 into a CFNumberRef. Caller then "owns" // the CFNumberRef and must CFRelease the CFNumberRef when done. func int32ToCFNumber(n int32) C.CFNumberRef { return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, unsafe.Pointer(&n)) } // Key is a wrapper around the Keychain reference that uses it to // implement signing-related methods with Keychain functionality. type Key struct { privateKeyRef C.SecKeyRef certs []*x509.Certificate once sync.Once publicKeyRef C.SecKeyRef hash crypto.Hash } // newKey makes a new Key wrapper around the key reference, // takes ownership of the reference, and sets up a finalizer to handle releasing // the reference. func newKey(privateKeyRef C.SecKeyRef, certs []*x509.Certificate, publicKeyRef C.SecKeyRef) (*Key, error) { k := &Key{ privateKeyRef: privateKeyRef, certs: certs, publicKeyRef: publicKeyRef, hash: crypto.SHA256, } // This struct now owns the key reference. Retain now and release on // finalise in case the credential gets forgotten about. C.CFRetain(C.CFTypeRef(privateKeyRef)) C.CFRetain(C.CFTypeRef(publicKeyRef)) runtime.SetFinalizer(k, func(x interface{}) { x.(*Key).Close() }) return k, nil } // CertificateChain returns the credential as a raw X509 cert chain. This // contains the public key. func (k *Key) CertificateChain() [][]byte { rv := make([][]byte, len(k.certs)) for i, c := range k.certs { rv[i] = c.Raw } return rv } // Close releases resources held by the credential. func (k *Key) Close() error { // Don't double-release references. k.once.Do(func() { C.CFRelease(C.CFTypeRef(k.privateKeyRef)) C.CFRelease(C.CFTypeRef(k.publicKeyRef)) }) return nil } // Public returns the corresponding public key for this Key. Good // thing we extracted it when we created it. func (k *Key) Public() crypto.PublicKey { return k.certs[0].PublicKey } // Sign signs a message digest. Here, we pass off the signing to Keychain library. func (k *Key) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { // Map the signing algorithm and hash function to a SecKeyAlgorithm constant. var algorithms map[crypto.Hash]C.CFStringRef switch pub := k.Public().(type) { case *ecdsa.PublicKey: algorithms = ecdsaAlgorithms case *rsa.PublicKey: if _, ok := opts.(*rsa.PSSOptions); ok { algorithms = rsaPSSAlgorithms break } algorithms = rsaPKCS1v15Algorithms default: return nil, fmt.Errorf("unsupported algorithm %T", pub) } algorithm, ok := algorithms[opts.HashFunc()] if !ok { return nil, fmt.Errorf("unsupported hash function %T", opts.HashFunc()) } // Copy input over into CF-land. cfDigest := bytesToCFData(digest) defer C.CFRelease(C.CFTypeRef(cfDigest)) var cfErr C.CFErrorRef sig := C.SecKeyCreateSignature(C.SecKeyRef(k.privateKeyRef), algorithm, C.CFDataRef(cfDigest), &cfErr) if cfErr != 0 { return nil, cfErrorFromRef(cfErr) } defer C.CFRelease(C.CFTypeRef(sig)) return cfDataToBytes(C.CFDataRef(sig)), nil } func getLoginKeychainPath() (string, error) { usr, err := user.Current() if err != nil { return "", fmt.Errorf("could not get current user: %w", err) } return filepath.Join(usr.HomeDir, "Library", "Keychains", "login.keychain-db"), nil } func getSystemKeychainPath() (string, error) { return "/Library/Keychains/System.keychain", nil } func getKeychainPath(keychainRef C.CFTypeRef) (string, error) { var pathBuf [1024]C.char pathLen := C.uint32_t(len(pathBuf)) status := C.SecKeychainGetPath(C.SecKeychainRef(keychainRef), &pathLen, &pathBuf[0]) if status != 0 { return "", fmt.Errorf("SecKeychainGetPath failed: %d", status) } return C.GoStringN(&pathBuf[0], C.int(pathLen)), nil } // findMatchingIdentities returns a list of identities satisfying the keychainType and issuerCN criteria as "leafIdents". // It also returns the parsed leaf certificates as "leafs", and a pointer of the underlying "leafMatches" to be released by the caller. func findMatchingIdentities(keychainType string, issuerCN string) ([]C.SecIdentityRef, []*x509.Certificate, C.CFTypeRef, error) { leafSearch := C.CFDictionaryCreateMutable(C.kCFAllocatorDefault, 5, &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(leafSearch))) // Get identities (certificate + private key pairs). C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassIdentity)) // Get identities that are signing capable. C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecAttrCanSign), unsafe.Pointer(C.kCFBooleanTrue)) // For each identity, give us the reference to it. C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecReturnRef), unsafe.Pointer(C.kCFBooleanTrue)) // Be sure to list out all the matches. C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitAll)) // Obtain the total keychain search space for the user as a list of keychains. var keychainList C.CFArrayRef if err := C.SecKeychainCopySearchList(&keychainList); err != C.errSecSuccess { return nil, nil, 0, fmt.Errorf("failed to get keychain search list: %w", keychainError(err)) } defer C.CFRelease(C.CFTypeRef(keychainList)) // Filter for login vs system keychain search space. if keychainType == "login" || keychainType == "system" { var targetPath string var err error if keychainType == "login" { targetPath, err = getLoginKeychainPath() } else { targetPath, err = getSystemKeychainPath() } if err != nil { return nil, nil, 0, fmt.Errorf("Error determining target keychain path: %w", err) } filteredKeychainList := C.CFArrayCreateMutable(C.kCFAllocatorDefault, 0, &C.kCFTypeArrayCallBacks) defer C.CFRelease(C.CFTypeRef(filteredKeychainList)) for i := 0; i < int(C.CFArrayGetCount(keychainList)); i++ { keychainRef := C.CFArrayGetValueAtIndex(keychainList, C.CFIndex(i)) keychainPath, err := getKeychainPath(C.CFTypeRef(keychainRef)) if err != nil { return nil, nil, 0, fmt.Errorf("Error extracting keychain path: %w", err) } if keychainPath == targetPath { C.CFArrayAppendValue(filteredKeychainList, keychainRef) } } keychainList = C.CFArrayRef(filteredKeychainList) } else if keychainType != "all" && keychainType != "" { return nil, nil, 0, fmt.Errorf("invalid keychain type: %s", keychainType) } // Restrict keychain search space C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecMatchSearchList), unsafe.Pointer(keychainList)) var leafMatches C.CFTypeRef if errno := C.SecItemCopyMatching(C.CFDictionaryRef(leafSearch), &leafMatches); errno != C.errSecSuccess { return nil, nil, 0, fmt.Errorf("failed to find matching identities: %w", keychainError(errno)) } signingIdents := C.CFArrayRef(leafMatches) var leafIdents []C.SecIdentityRef var leafs []*x509.Certificate for i := 0; i < int(C.CFArrayGetCount(signingIdents)); i++ { identDict := C.CFArrayGetValueAtIndex(signingIdents, C.CFIndex(i)) xc, err := identityToX509(C.SecIdentityRef(identDict)) if err != nil { continue // Skip this identity if there's an error } if xc.Issuer.CommonName == issuerCN { leafs = append(leafs, xc) leafIdents = append(leafIdents, C.SecIdentityRef(identDict)) } } return leafIdents, leafs, leafMatches, nil } // compareCertificatesByRaw compares two certificates for exact byte-for-byte equality. // It returns true if and only if the certificates have identical DER-encoded representations. func compareCertificatesByRaw(cert1, cert2 *x509.Certificate) bool { if cert1 == nil || cert2 == nil { return cert1 == cert2 // True only if both are nil } return bytes.Equal(cert1.Raw, cert2.Raw) } // Cred gets the first Credential (filtering on issuer and keychainType) corresponding to // available certificate and private key pairs (i.e. identities) in // the Keychain. Accepted values for keychainType are "login", "system", and "all". // For backwards compatibility, an empty keychainType will be treated as "all". func Cred(issuerCN, keychainType string) (*Key, error) { leafIdents, leafs, leafMatches, err := findMatchingIdentities(keychainType, issuerCN) if err != nil { return nil, err } defer C.CFRelease(leafMatches) // If system keychain, we need to do an extra query for login, and subtract that from the final results. // This is because of a quirk with Apple's kSecMatchSearchList API, which incorrectly returns results // from both the login and system keychain when we retrict the search space to system only. if keychainType == "system" { loginLeafIdents, _, loginLeafMatches, err := findMatchingIdentities("login", issuerCN) if err != nil { return nil, err } defer C.CFRelease(loginLeafMatches) var filteredLeafIdents []C.SecIdentityRef var filteredLeafs []*x509.Certificate outerLoop: for i, systemIdent := range leafIdents { systemCert, err1 := identityToX509(systemIdent) if err1 != nil { continue // Skip if we can't get the certificate } for _, loginIdent := range loginLeafIdents { loginCert, err2 := identityToX509(loginIdent) if err2 != nil { continue //Skip if we can't get the certificate } if compareCertificatesByRaw(systemCert, loginCert) { continue outerLoop // Found a match, skip this login identity. } } // If we get here, no match was found in loginLeafIdents, so it's safe to append to our filtered results. filteredLeafIdents = append(filteredLeafIdents, systemIdent) filteredLeafs = append(filteredLeafs, leafs[i]) } leafIdents = filteredLeafIdents leafs = filteredLeafs } var leaf *x509.Certificate var leafIdent C.SecIdentityRef // Select the first match from the final results. if len(leafs) > 0 { leaf = leafs[0] leafIdent = leafIdents[0] } else { return nil, fmt.Errorf("no key found with issuer common name %q", issuerCN) } caSearch := C.CFDictionaryCreateMutable(C.kCFAllocatorDefault, 0, &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(caSearch))) // Get identities (certificates). C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassCertificate)) // For each identity, give us the reference to it. C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecReturnRef), unsafe.Pointer(C.kCFBooleanTrue)) // Be sure to list out all the matches. C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitAll)) // Do the matching-item copy. var caMatches C.CFTypeRef if errno := C.SecItemCopyMatching((C.CFDictionaryRef)(caSearch), &caMatches); errno != C.errSecSuccess { return nil, keychainError(errno) } defer C.CFRelease(caMatches) certRefs := C.CFArrayRef(caMatches) // Validate and dump the certs into golang x509 Certificates. var allCerts []*x509.Certificate for i := 0; i < int(C.CFArrayGetCount(certRefs)); i++ { refDict := C.CFArrayGetValueAtIndex(certRefs, C.CFIndex(i)) if xc, err := certRefToX509(C.SecCertificateRef(refDict)); err == nil { allCerts = append(allCerts, xc) } } // Build a certificate chain from leaf by matching prev.RawIssuer to // next.RawSubject across all valid certificates in the keychain. var ( certs []*x509.Certificate prev, next *x509.Certificate ) for prev = leaf; prev != nil; prev, next = next, nil { certs = append(certs, prev) for _, xc := range allCerts { if certIn(xc, certs) { continue // finite chains only, mmmmkay. } if bytes.Equal(prev.RawIssuer, xc.RawSubject) && prev.CheckSignatureFrom(xc) == nil { // Prefer certificates with later expirations. if next == nil || xc.NotAfter.After(next.NotAfter) { next = xc } } } } if len(certs) == 0 { return nil, fmt.Errorf("no key found with issuer common name %q", issuerCN) } skr, err := identityToPrivateSecKeyRef(leafIdent) if err != nil { return nil, err } pubKey, err := identityToPublicSecKeyRef(leafIdent) if err != nil { return nil, err } defer C.CFRelease(C.CFTypeRef(skr)) return newKey(skr, certs, pubKey) } // identityToX509 converts a single CFDictionary that contains the item ref and // attribute dictionary into an x509.Certificate. func identityToX509(ident C.SecIdentityRef) (*x509.Certificate, error) { var certRef C.SecCertificateRef if errno := C.SecIdentityCopyCertificate(ident, &certRef); errno != 0 { return nil, keychainError(errno) } defer C.CFRelease(C.CFTypeRef(certRef)) return certRefToX509(certRef) } // certRefToX509 converts a single C.SecCertificateRef into an *x509.Certificate. func certRefToX509(certRef C.SecCertificateRef) (*x509.Certificate, error) { // Export the PEM-encoded certificate to a CFDataRef. var certPEMData C.CFDataRef if errno := C.SecItemExport(C.CFTypeRef(certRef), C.kSecFormatUnknown, C.kSecItemPemArmour, nil, &certPEMData); errno != 0 { return nil, keychainError(errno) } defer C.CFRelease(C.CFTypeRef(certPEMData)) certPEM := cfDataToBytes(certPEMData) // This part based on crypto/tls. var certDERBlock *pem.Block for { certDERBlock, certPEM = pem.Decode(certPEM) if certDERBlock == nil { return nil, fmt.Errorf("failed to parse certificate PEM data") } if certDERBlock.Type == "CERTIFICATE" { // found it break } } // Check the certificate is OK by the x509 library, and obtain the // public key algorithm (which I assume is the same as the private key // algorithm). This also filters out certs missing critical extensions. xc, err := x509.ParseCertificate(certDERBlock.Bytes) if err != nil { return nil, err } switch xc.PublicKey.(type) { case *rsa.PublicKey, *ecdsa.PublicKey: default: return nil, fmt.Errorf("unsupported key type %T", xc.PublicKey) } // Check the certificate is valid if n := time.Now(); n.Before(xc.NotBefore) || n.After(xc.NotAfter) { return nil, fmt.Errorf("certificate not valid") } return xc, nil } // identityToSecKeyRef converts a single CFDictionary that contains the item ref and // attribute dictionary into a SecKeyRef for its private key. func identityToPrivateSecKeyRef(ident C.SecIdentityRef) (C.SecKeyRef, error) { // Get the private key (ref). Note that "Copy" in "CopyPrivateKey" // refers to "the create rule" of CoreFoundation memory management, and // does not actually copy the private key---it gives us a copy of the // reference that we now own. var ref C.SecKeyRef if errno := C.SecIdentityCopyPrivateKey(C.SecIdentityRef(ident), &ref); errno != 0 { return 0, keychainError(errno) } return ref, nil } func identityToPublicSecKeyRef(ident C.SecIdentityRef) (C.SecKeyRef, error) { var key C.SecKeyRef var certRef C.SecCertificateRef if errno := C.SecIdentityCopyCertificate(ident, &certRef); errno != 0 { return 0, keychainError(errno) } defer C.CFRelease(C.CFTypeRef(certRef)) key = C.SecCertificateCopyKey(certRef) if key == invalidKey { return 0, fmt.Errorf("public key was NULL. Key might have an encoding issue or use an unsupported algorithm") } return key, nil } func stringIn(s string, ss []string) bool { for _, s2 := range ss { if s == s2 { return true } } return false } func certIn(xc *x509.Certificate, xcs []*x509.Certificate) bool { for _, xc2 := range xcs { if xc.Equal(xc2) { return true } } return false } func (k *Key) getPaddingSize() int { algorithms, algoErr := k.getEncryptAlgorithm() if algoErr != nil { fmt.Printf("algorithm is unsupported. only RSA algorithms are supported. %v", algoErr) } // Each padding scheme has varying number of bytes. pssPaddingBytes := 20 oaepPaddingBytes := 130 pkcsPaddingBytes := 11 switch algorithms { case C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256, C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384, C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512: return pssPaddingBytes case C.kSecKeyAlgorithmRSAEncryptionOAEPSHA256, C.kSecKeyAlgorithmRSAEncryptionOAEPSHA384, C.kSecKeyAlgorithmRSAEncryptionOAEPSHA512: return oaepPaddingBytes case C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256, C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384, C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512: return pkcsPaddingBytes default: return int(unknownSecKeyAlgorithm) } } func (k *Key) checkDataSize(plaintext []byte) error { // Plaintext data must be smaller than the key's block size minus padding space. sizeLim := uint64(C.SecKeyGetBlockSize(k.publicKeyRef)) - uint64(k.getPaddingSize()) if uint64(len(plaintext)) >= sizeLim { return fmt.Errorf("plaintext is too long") } return nil } func (k *Key) getRSAEncryptAlgorithm() (C.SecKeyAlgorithm, error) { var algorithms map[crypto.Hash]C.CFStringRef switch pub := k.Public().(type) { case *rsa.PublicKey: if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeEncrypt, C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256) == 1 { algorithms = rsaPSSAlgorithms } else if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeEncrypt, C.kSecKeyAlgorithmRSAEncryptionOAEPSHA256) == 1 { algorithms = rsaOAEPAlgorithms } else if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeEncrypt, C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256) == 1 { algorithms = rsaPKCS1v15Algorithms } else { return unknownSecKeyAlgorithm, fmt.Errorf("unknown RSA argument. Only supports PSS, OAEP, and PKCS1v1.5 %T", pub) } default: return unknownSecKeyAlgorithm, fmt.Errorf("algorithm is unsupported. only RSA algorithms are supported. %T", pub) } return algorithms[k.hash], nil } func (k *Key) getEncryptAlgorithm() (C.SecKeyAlgorithm, error) { if k.hash == 0 { k.hash = crypto.SHA256 } return k.getRSAEncryptAlgorithm() } func (k *Key) getRSADecryptAlgorithm() (C.SecKeyAlgorithm, error) { var algorithms map[crypto.Hash]C.CFStringRef switch pub := k.Public().(type) { case *rsa.PublicKey: if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeDecrypt, C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256) == 1 { algorithms = rsaPSSAlgorithms } else if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeDecrypt, C.kSecKeyAlgorithmRSAEncryptionOAEPSHA256) == 1 { algorithms = rsaOAEPAlgorithms } else if C.SecKeyIsAlgorithmSupported(k.publicKeyRef, C.kSecKeyOperationTypeDecrypt, C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256) == 1 { algorithms = rsaPKCS1v15Algorithms } else { return unknownSecKeyAlgorithm, fmt.Errorf("unknown RSA argument. Only supports PSS, OAEP, and PKCS1v1.5 %T", pub) } default: return unknownSecKeyAlgorithm, fmt.Errorf("algorithm is unsupported. only RSA algorithms are supported. %T", pub) } return algorithms[k.hash], nil } func (k *Key) getDecryptAlgorithm() (C.SecKeyAlgorithm, error) { return k.getRSADecryptAlgorithm() } // Encrypt encrypts a plaintext message digest using the public key. Here, we pass off the encryption to Keychain library. func (k *Key) Encrypt(plaintext []byte, opts any) ([]byte, error) { if hash, ok := opts.(crypto.Hash); ok { k.hash = hash } else { return nil, fmt.Errorf("Unsupported encrypt opts: %v", opts) } pub := k.publicKeyRef algorithm, err := k.getEncryptAlgorithm() if err != nil { return nil, err } if err := k.checkDataSize(plaintext); err != nil { return nil, err } msg := bytesToCFData(plaintext) var cfErr C.CFErrorRef bytes := C.SecKeyCreateEncryptedData(pub, algorithm, msg, &cfErr) if cfErr != 0 { return nil, cfErrorFromRef(cfErr) } ciphertext := cfDataToBytes(bytes) return ciphertext, cfErrorFromRef(cfErr) } // Decrypt decrypts a ciphertext message digest using the private key. Here, we pass off the decryption to Keychain library. // Currently, only *rsa.OAEPOptions is supported for opts. func (k *Key) Decrypt(ciphertext []byte, opts crypto.DecrypterOpts) ([]byte, error) { if oaepOpts, ok := opts.(*rsa.OAEPOptions); ok { k.hash = oaepOpts.Hash } else { return nil, fmt.Errorf("Unsupported DecrypterOpts: %v", opts) } priv := k.privateKeyRef algorithm, err := k.getDecryptAlgorithm() if err != nil { return nil, err } msg := bytesToCFData(ciphertext) var cfErr C.CFErrorRef bytes := C.SecKeyCreateDecryptedData(priv, algorithm, msg, &cfErr) if cfErr != 0 { return nil, cfErrorFromRef(cfErr) } plaintext := cfDataToBytes(bytes) return plaintext, cfErrorFromRef(cfErr) } var osStatusDescriptions = map[C.OSStatus]string{ C.errSecSuccess: "No error", C.errSecUnimplemented: "Function or operation not implemented.", C.errSecParam: "One or more parameters passed to the function were not valid.", C.errSecAllocate: "Failed to allocate memory.", C.errSecNotAvailable: "No keychain is available. You may need to restart your computer.", C.errSecDuplicateItem: "The specified item already exists in the keychain.", C.errSecItemNotFound: "The specified item could not be found in the keychain.", C.errSecInteractionNotAllowed: "User interaction is not allowed.", C.errSecDecode: "Unable to decode the provided data.", C.errSecPolicyNotFound: "The specified policy could not be found.", C.errSecPkcs12VerifyFailure: "MAC verification failed during PKCS12 import (wrong password?)", } // Helper function to get OSStatus description // See (https://cdn.nsoftware.com/help/legacy/sbb/ref_err_appleerrorcodes.html) func osStatusDescription(status C.OSStatus) string { if description, ok := osStatusDescriptions[status]; ok { return description } return "Unknown OSStatus" } // ImportPKCS12Cred imports a PKCS12 file containing a client certificate and private key into the keychain func ImportPKCS12Cred(credPath string, password string) error { // 1. Load the .p12 file keyData, err := os.ReadFile(credPath) if err != nil { return fmt.Errorf("error reading private key file: %w", err) } // 2. Create options dictionary with password optionsKeys := []C.CFTypeRef{ C.CFTypeRef(C.kSecImportExportPassphrase), } optionsValues := []C.CFTypeRef{ C.CFTypeRef(C.CFStringCreateWithCString(C.kCFAllocatorDefault, C.CString(password), C.kCFStringEncodingUTF8)), } optionsDict := C.CFDictionaryCreate(C.kCFAllocatorDefault, (*unsafe.Pointer)(unsafe.Pointer(&optionsKeys[0])), (*unsafe.Pointer)(unsafe.Pointer(&optionsValues[0])), C.CFIndex(len(optionsKeys)), &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks, ) defer C.CFRelease(C.CFTypeRef(optionsDict)) // 3. Import the .p12 data with password status := C.SecPKCS12Import(bytesToCFData(keyData), optionsDict, nil) if status != C.errSecSuccess { return fmt.Errorf("failed to import PKCS#12 data: %s", osStatusDescription(status)) } return nil } enterprise-certificate-proxy-0.3.6/internal/signer/darwin/keychain/keychain_test.go000066400000000000000000000064021476410620300307070ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. //go:build darwin && cgo // +build darwin,cgo package keychain import ( "bytes" "crypto" "crypto/rsa" "testing" "unsafe" ) const testIssuer = "TestIssuer" func TestKeychainError(t *testing.T) { tests := []struct { e keychainError want string }{ {e: keychainError(0), want: "No error."}, {e: keychainError(-4), want: "Function or operation not implemented."}, } for i, test := range tests { if got := test.e.Error(); got != test.want { t.Errorf("test %d: %#v.Error() = %q, want %q", i, test.e, got, test.want) } } } func TestBytesToCFDataRoundTrip(t *testing.T) { want := []byte("an arbitrary and yet coherent byte slice!") d := bytesToCFData(want) defer cfRelease(unsafe.Pointer(d)) if got := cfDataToBytes(d); !bytes.Equal(got, want) { t.Errorf("bytesToCFData -> cfDataToBytes\ngot %x\nwant %x", got, want) } } func TestImportPKCS12Cred(t *testing.T) { credPath := "../../../../testdata/testcred.p12" password := "1234" err := ImportPKCS12Cred(credPath, password) if err != nil { t.Errorf("ImportPKCS12Cred: got %v, want nil err", err) return } } func TestEncrypt(t *testing.T) { key, err := Cred(testIssuer, "") if err != nil { t.Errorf("Cred: got %v, want nil err", err) return } plaintext := []byte("Plain text to encrypt") _, err = key.Encrypt(plaintext, crypto.SHA256) if err != nil { t.Errorf("Encrypt: got %v, want nil err", err) return } } func BenchmarkEncrypt(b *testing.B) { key, err := Cred(testIssuer, "") if err != nil { b.Errorf("Cred: got %v, want nil err", err) return } plaintext := []byte("Plain text to encrypt") for i := 0; i < b.N; i++ { _, err := key.Encrypt(plaintext, crypto.SHA256) if err != nil { b.Errorf("Encrypt: got %v, want nil err", err) } } } func TestDecrypt(t *testing.T) { key, err := Cred(testIssuer, "") if err != nil { t.Errorf("Cred: got %v, want nil err", err) return } byteSlice := []byte("Plain text to encrypt") ciphertext, _ := key.Encrypt(byteSlice, crypto.SHA256) plaintext, err := key.Decrypt(ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA256}) if err != nil { t.Errorf("Decrypt: got %v, want nil err", err) return } if !bytes.Equal(byteSlice, plaintext) { t.Errorf("Decryption message does not match original: got %v, want %v", plaintext, byteSlice) } } func BenchmarkDecrypt(b *testing.B) { key, err := Cred(testIssuer, "") if err != nil { b.Errorf("Cred: got %v, want nil err", err) return } byteSlice := []byte("Plain text to encrypt") ciphertext, _ := key.Encrypt(byteSlice, crypto.SHA256) for i := 0; i < b.N; i++ { _, err := key.Decrypt(ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA256}) if err != nil { b.Errorf("Decrypt: got %v, want nil err", err) } } } enterprise-certificate-proxy-0.3.6/internal/signer/darwin/signer.go000066400000000000000000000115531476410620300255540ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // Signer.go is a net/rpc server that listens on stdin/stdout, exposing // methods that perform device certificate signing for Mac OS using keychain utils. // This server is intended to be launched as a subprocess by the signer client, // and should not be launched manually as a stand-alone process. package main import ( "crypto" "crypto/rsa" "crypto/x509" "encoding/gob" "io" "log" "net/rpc" "os" "time" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/darwin/keychain" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/util" ) // If ECP Logging is enabled return true // Otherwise return false func enableECPLogging() bool { if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { return true } log.SetOutput(io.Discard) return false } func init() { gob.Register(crypto.SHA256) gob.Register(crypto.SHA384) gob.Register(crypto.SHA512) gob.Register(&rsa.PSSOptions{}) gob.Register(&rsa.OAEPOptions{}) } // SignArgs contains arguments for a Sign API call. type SignArgs struct { Digest []byte // The content to sign. Opts crypto.SignerOpts // Options for signing. Must implement HashFunc(). } // EncryptArgs contains arguments for an Encrypt API call. type EncryptArgs struct { Plaintext []byte // The plaintext to encrypt. Opts any // Options for encryption. Ex: an instance of crypto.Hash. } // DecryptArgs contains arguments to for a Decrypt API call. type DecryptArgs struct { Ciphertext []byte // The ciphertext to decrypt. Opts crypto.DecrypterOpts // Options for decryption. Ex: an instance of *rsa.OAEPOptions. } // A EnterpriseCertSigner exports RPC methods for signing. type EnterpriseCertSigner struct { key *keychain.Key } // A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. type Connection struct { io.ReadCloser io.WriteCloser } // Close closes c's underlying ReadCloser and WriteCloser. func (c *Connection) Close() error { rerr := c.ReadCloser.Close() werr := c.WriteCloser.Close() if rerr != nil { return rerr } return werr } // CertificateChain returns the credential as a raw X509 cert chain. This // contains the public key. func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { *certificateChain = k.key.CertificateChain() return nil } // Public returns the corresponding public key for this Key, in ASN.1 DER form. func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { *publicKey, err = x509.MarshalPKIXPublicKey(k.key.Public()) return } // Sign signs a message digest. Stores result in "resp". func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { *resp, err = k.key.Sign(nil, args.Digest, args.Opts) return } // Encrypt encrypts a plaintext message digest. Stores result in "resp". func (k *EnterpriseCertSigner) Encrypt(args EncryptArgs, resp *[]byte) (err error) { *resp, err = k.key.Encrypt(args.Plaintext, args.Opts) return } // Decrypt decrypts a ciphertext message digest. Stores result in "resp". func (k *EnterpriseCertSigner) Decrypt(args DecryptArgs, resp *[]byte) (err error) { *resp, err = k.key.Decrypt(args.Ciphertext, args.Opts) return } func main() { enableECPLogging() if len(os.Args) != 2 { log.Fatalln("Signer is not meant to be invoked manually, exiting...") } configFilePath := os.Args[1] config, err := util.LoadConfig(configFilePath) if err != nil { log.Fatalf("Failed to load enterprise cert config: %v", err) } enterpriseCertSigner := new(EnterpriseCertSigner) enterpriseCertSigner.key, err = keychain.Cred(config.CertConfigs.MacOSKeychain.Issuer, config.CertConfigs.MacOSKeychain.KeychainType) if err != nil { log.Fatalf("Failed to initialize enterprise cert signer using keychain: %v", err) } if err := rpc.Register(enterpriseCertSigner); err != nil { log.Fatalf("Failed to register enterprise cert signer with net/rpc: %v", err) } // If the parent process dies, we should exit. // We can detect this by periodically checking if the PID of the parent // process is 1 (https://stackoverflow.com/a/2035683). go func() { for { if os.Getppid() == 1 { log.Fatalln("Enterprise cert signer's parent process died, exiting...") } time.Sleep(time.Second) } }() rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) } enterprise-certificate-proxy-0.3.6/internal/signer/linux/000077500000000000000000000000001476410620300236045ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/linux/pkcs11/000077500000000000000000000000001476410620300247065ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/linux/pkcs11/pkcs11.go000066400000000000000000000142151476410620300263420ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // pkcs11 provides helpers for working with certificates via PKCS#11 APIs // provided by go-pkcs11 package pkcs11 import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "errors" "fmt" "hash" "io" "strconv" "strings" "github.com/google/go-pkcs11/pkcs11" ) // ParseHexString parses hexadecimal string into uint32 func ParseHexString(str string) (i uint32, err error) { stripped := strings.Replace(str, "0x", "", -1) resultUint64, err := strconv.ParseUint(stripped, 16, 32) if err != nil { return 0, err } return uint32(resultUint64), nil } // Cred returns a Key wrapping the first valid certificate in the pkcs11 module // matching a given slot and label. func Cred(pkcs11Module string, slotUint32Str string, label string, userPin string) (*Key, error) { module, err := pkcs11.Open(pkcs11Module) if err != nil { return nil, err } slotUint32, err := ParseHexString(slotUint32Str) if err != nil { return nil, err } kslot, err := module.Slot(slotUint32, pkcs11.Options{PIN: userPin}) if err != nil { return nil, err } certs, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassCertificate, Label: label}) if err != nil { return nil, err } if len(certs) < 1 { return nil, fmt.Errorf("No certificate object was found with label %s.", label) } cert, err := certs[0].Certificate() if err != nil { return nil, err } x509, err := cert.X509() if err != nil { return nil, err } var kchain [][]byte kchain = append(kchain, x509.Raw) pubKeys, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassPublicKey, Label: label}) if err != nil { return nil, err } if len(pubKeys) < 1 { return nil, fmt.Errorf("No public key object was found with label %s.", label) } pubKey, err := pubKeys[0].PublicKey() if err != nil { return nil, err } privkeys, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassPrivateKey, Label: label}) if err != nil { return nil, err } if len(privkeys) < 1 { return nil, fmt.Errorf("No private key object was found with label %s.", label) } privKey, err := privkeys[0].PrivateKey(pubKey) if err != nil { return nil, err } ksigner, ok := privKey.(crypto.Signer) if !ok { return nil, errors.New("PrivateKey does not implement crypto.Signer") } kdecrypter, _ := privKey.(crypto.Decrypter) defaultHash := crypto.SHA256 return &Key{ slot: kslot, signer: ksigner, chain: kchain, privKey: privKey, label: label, module: *module, hash: defaultHash, decrypter: kdecrypter, }, nil } // Key is a wrapper around the pkcs11 module and uses it to // implement signing-related methods. type Key struct { slot *pkcs11.Slot signer crypto.Signer chain [][]byte privKey crypto.PrivateKey label string module pkcs11.Module hash crypto.Hash decrypter crypto.Decrypter } // CertificateChain returns the credential as a raw X509 cert chain. This // contains the public key. func (k *Key) CertificateChain() [][]byte { return k.chain } // Close releases resources held by the credential. func (k *Key) Close() { k.slot.Close() k.module.Close() } // Public returns the corresponding public key for this Key. func (k *Key) Public() crypto.PublicKey { return k.signer.Public() } // Sign signs a message. func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return k.signer.Sign(nil, digest, opts) } // Encrypt encrypts a plaintext message digest using the public key. Here, we use standard golang API. func (k *Key) Encrypt(plaintext []byte, opts any) ([]byte, error) { if hash, ok := opts.(crypto.Hash); ok { k.hash = hash } else { return nil, fmt.Errorf("Unsupported encrypt opts: %v", opts) } publicKey := k.Public() _, ok := publicKey.(*rsa.PublicKey) if ok { return k.encryptRSA(plaintext) } _, ok = publicKey.(*ecdsa.PublicKey) if ok { // TODO: Implement encryption for ec keys - https://github.com/googleapis/enterprise-certificate-proxy/issues/95 return nil, errors.New("encrypt error: EC keys not yet supported") } return nil, errors.New("encrypt error: Unsupported key type") } // Decrypt decrypts a ciphertext message digest using the private key. Here, we pass off the decryption to pkcs11 library. func (k *Key) Decrypt(msg []byte, opts crypto.DecrypterOpts) ([]byte, error) { if oaepOpts, ok := opts.(*rsa.OAEPOptions); ok { k.hash = oaepOpts.Hash } else { return nil, fmt.Errorf("Unsupported DecrypterOpts: %v", opts) } if k.decrypter == nil { return nil, fmt.Errorf("decrypt error: Decrypter is nil") } publicKey := k.Public() _, ok := publicKey.(*rsa.PublicKey) if ok { return k.decryptRSAWithPKCS11(msg) } _, ok = publicKey.(*ecdsa.PublicKey) if ok { // TODO: Implement decryption for ec keys - https://github.com/googleapis/enterprise-certificate-proxy/issues/95 return nil, errors.New("decrypt error: EC keys not yet supported") } return nil, errors.New("decrypt error: Unsupported key type") } func (k *Key) encryptRSA(data []byte) ([]byte, error) { publicKey := k.Public() rsaPubKey := publicKey.(*rsa.PublicKey) hash, err := cryptoHashToHash(k.hash) if err != nil { return nil, err } return rsa.EncryptOAEP(hash, rand.Reader, rsaPubKey, data, nil) } func (k *Key) decryptRSAWithPKCS11(encryptedData []byte) ([]byte, error) { opts := &rsa.OAEPOptions{Hash: k.hash} return k.decrypter.Decrypt(nil, encryptedData, opts) } func cryptoHashToHash(hash crypto.Hash) (hash.Hash, error) { switch hash { case crypto.SHA256: return sha256.New(), nil case crypto.SHA1: return sha1.New(), nil default: return nil, errors.New("hash conversion error: Unsupported hash") } } enterprise-certificate-proxy-0.3.6/internal/signer/linux/pkcs11/pkcs11_test.go000066400000000000000000000055111476410620300274000ustar00rootroot00000000000000// Copyright 2023 Google LLC. // 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 // // https://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. package pkcs11 import ( "bytes" "crypto" "crypto/rsa" "flag" "testing" ) const ( testModule = "/usr/lib/softhsm/libsofthsm2.so" testLabel = "Demo Object" testUserPin = "0000" ) var testSlot = flag.String("testSlot", "", "libsofthsm2 slot location") func makeTestKey() (*Key, error) { key, err := Cred(testModule, *testSlot, testLabel, testUserPin) return key, err } func TestParseHexString(t *testing.T) { got, err := ParseHexString("0x1739427") if err != nil { t.Fatalf("ParseHexString error: %v", err) } want := uint32(0x1739427) if got != want { t.Errorf("Expected result is %v, got: %v", want, got) } } func TestParseHexStringFailure(t *testing.T) { _, err := ParseHexString("abcdefgh") if err == nil { t.Error("Expected error but got nil") } } func TestCredLinux(t *testing.T) { key, err := makeTestKey() if err != nil { t.Errorf("Cred error: %q", err) } defer key.Close() } func BenchmarkEncryptRSA(b *testing.B) { msg := "Plain text to encrypt" bMsg := []byte(msg) key, errCred := makeTestKey() if errCred != nil { b.Errorf("Cred error: %q", errCred) return } defer key.Close() b.Run("encryptRSA Crypto", func(b *testing.B) { for i := 0; i < b.N; i++ { _, errEncrypt := key.encryptRSA(bMsg) if errEncrypt != nil { b.Errorf("EncryptRSA error: %q", errEncrypt) return } } }) } func TestEncrypt(t *testing.T) { key, errCred := makeTestKey() if errCred != nil { t.Errorf("Cred error: %q", errCred) return } defer key.Close() msg := "Plain text to encrypt" bMsg := []byte(msg) _, err := key.Encrypt(bMsg, crypto.SHA1) if err != nil { t.Errorf("Encrypt error: %q", err) } } func TestDecrypt(t *testing.T) { key, errCred := makeTestKey() if errCred != nil { t.Errorf("Cred error: %q", errCred) return } defer key.Close() msg := "Plain text to encrypt" bMsg := []byte(msg) // Softhsm only supports SHA1 ciphertext, err := key.Encrypt(bMsg, crypto.SHA1) if err != nil { t.Errorf("Encrypt error: %q", err) } decrypted, err := key.Decrypt(ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA1}) if err != nil { t.Fatalf("Decrypt error: %v", err) } decrypted = bytes.Trim(decrypted, "\x00") if string(decrypted) != msg { t.Errorf("Decrypt error: expected %q, got %q", msg, string(decrypted)) } } enterprise-certificate-proxy-0.3.6/internal/signer/linux/signer.go000066400000000000000000000116171476410620300254300ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // Signer.go is a net/rpc server that listens on stdin/stdout, exposing // methods that perform device certificate signing for Linux using PKCS11 // shared library. // This server is intended to be launched as a subprocess by the signer client, // and should not be launched manually as a stand-alone process. package main import ( "crypto" "crypto/rsa" "crypto/x509" "encoding/gob" "io" "log" "net/rpc" "os" "time" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/linux/pkcs11" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/util" ) // If ECP Logging is enabled return true // Otherwise return false func enableECPLogging() bool { if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { return true } log.SetOutput(io.Discard) return false } func init() { gob.Register(crypto.SHA256) gob.Register(crypto.SHA384) gob.Register(crypto.SHA512) gob.Register(&rsa.PSSOptions{}) gob.Register(&rsa.OAEPOptions{}) } // SignArgs contains arguments for a Sign API call. type SignArgs struct { Digest []byte // The content to sign. Opts crypto.SignerOpts // Options for signing. Must implement HashFunc(). } // EncryptArgs contains arguments for an Encrypt API call. type EncryptArgs struct { Plaintext []byte // The plaintext to encrypt. Opts any // Options for encryption. Ex: an instance of crypto.Hash. } // DecryptArgs contains arguments to for a Decrypt API call. type DecryptArgs struct { Ciphertext []byte // The ciphertext to decrypt. Opts crypto.DecrypterOpts // Options for decryption. Ex: an instance of *rsa.OAEPOptions. } // A EnterpriseCertSigner exports RPC methods for signing. type EnterpriseCertSigner struct { key *pkcs11.Key } // A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. type Connection struct { io.ReadCloser io.WriteCloser } // Close closes c's underlying ReadCloser and WriteCloser. func (c *Connection) Close() error { rerr := c.ReadCloser.Close() werr := c.WriteCloser.Close() if rerr != nil { return rerr } return werr } // CertificateChain returns the credential as a raw X509 cert chain. This // contains the public key. func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) (err error) { *certificateChain = k.key.CertificateChain() return nil } // Public returns the corresponding public key for this Key, in ASN.1 DER form. func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { *publicKey, err = x509.MarshalPKIXPublicKey(k.key.Public()) return } // Sign signs a message digest. Stores result in "resp". func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { *resp, err = k.key.Sign(nil, args.Digest, args.Opts) return } // Encrypt encrypts a plaintext msg. Stores result in "resp". func (k *EnterpriseCertSigner) Encrypt(args EncryptArgs, resp *[]byte) (err error) { *resp, err = k.key.Encrypt(args.Plaintext, args.Opts) return } // Decrypt decrypts a ciphertext msg. Stores result in "resp". func (k *EnterpriseCertSigner) Decrypt(args DecryptArgs, resp *[]byte) (err error) { *resp, err = k.key.Decrypt(args.Ciphertext, args.Opts) return } func main() { enableECPLogging() if len(os.Args) != 2 { log.Fatalln("Signer is not meant to be invoked manually, exiting...") } configFilePath := os.Args[1] config, err := util.LoadConfig(configFilePath) if err != nil { log.Fatalf("Failed to load enterprise cert config: %v", err) } enterpriseCertSigner := new(EnterpriseCertSigner) enterpriseCertSigner.key, err = pkcs11.Cred(config.CertConfigs.PKCS11.PKCS11Module, config.CertConfigs.PKCS11.Slot, config.CertConfigs.PKCS11.Label, config.CertConfigs.PKCS11.UserPin) if err != nil { log.Fatalf("Failed to initialize enterprise cert signer using pkcs11: %v", err) } if err := rpc.Register(enterpriseCertSigner); err != nil { log.Fatalf("Failed to register enterprise cert signer with net/rpc: %v", err) } // If the parent process dies, we should exit. // We can detect this by periodically checking if the PID of the parent // process is 1 (https://stackoverflow.com/a/2035683). go func() { for { if os.Getppid() == 1 { log.Fatalln("Enterprise cert signer's parent process died, exiting...") } time.Sleep(time.Second) } }() rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) } enterprise-certificate-proxy-0.3.6/internal/signer/test/000077500000000000000000000000001476410620300234245ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/test/signer.go000066400000000000000000000070261476410620300252470ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // signer.go is a net/rpc server that listens on stdin/stdout, exposing // mock methods for testing client.go. package main import ( "crypto" "crypto/tls" "crypto/x509" "io" "log" "net/rpc" "os" "time" ) // SignArgs encapsulate the parameters for the Sign method. type SignArgs struct { Digest []byte Opts crypto.SignerOpts } // EncryptArgs encapsulate the parameters for the Encrypt method. type EncryptArgs struct { Plaintext []byte } // DecryptArgs encapsulate the parameters for the Decrypt method. type DecryptArgs struct { Ciphertext []byte } // EnterpriseCertSigner exports RPC methods for signing. type EnterpriseCertSigner struct { cert *tls.Certificate } // Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. type Connection struct { io.ReadCloser io.WriteCloser } // Close closes c's underlying ReadCloser and WriteCloser. func (c *Connection) Close() error { rerr := c.ReadCloser.Close() werr := c.WriteCloser.Close() if rerr != nil { return rerr } return werr } // CertificateChain returns the credential as a raw X509 cert chain. This // contains the public key. func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { *certificateChain = k.cert.Certificate return nil } // Public returns the first public key for this Key, in ASN.1 DER form. func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { if len(k.cert.Certificate) == 0 { return nil } cert, err := x509.ParseCertificate(k.cert.Certificate[0]) if err != nil { return err } *publicKey, err = x509.MarshalPKIXPublicKey(cert.PublicKey) return err } // Sign signs a message digest. For testing, we return the input as-is. func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { *resp = args.Digest return nil } // Encrypt encrypts a plaintext msg. For testing, we return the input as-is. func (k *EnterpriseCertSigner) Encrypt(args EncryptArgs, plaintext *[]byte) (err error) { *plaintext = args.Plaintext return nil } // Decrypt decrypts a ciphertext msg. For testing, we return the input as-is. func (k *EnterpriseCertSigner) Decrypt(args DecryptArgs, ciphertext *[]byte) (err error) { *ciphertext = args.Ciphertext return nil } func main() { enterpriseCertSigner := new(EnterpriseCertSigner) data, err := os.ReadFile(os.Args[1]) if err != nil { log.Fatalf("Error reading certificate: %v", err) } cert, _ := tls.X509KeyPair(data, data) enterpriseCertSigner.cert = &cert if err := rpc.Register(enterpriseCertSigner); err != nil { log.Fatalf("Error registering net/rpc: %v", err) } // If the parent process dies, we should exit. // We can detect this by periodically checking if the PID of the parent // process is 1 (https://stackoverflow.com/a/2035683). go func() { for { if os.Getppid() == 1 { log.Fatalln("Parent process died, exiting...") } time.Sleep(time.Second) } }() rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) } enterprise-certificate-proxy-0.3.6/internal/signer/util/000077500000000000000000000000001476410620300234225ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/util/test_data/000077500000000000000000000000001476410620300253725ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/util/test_data/certificate_config.json000066400000000000000000000006221476410620300320740ustar00rootroot00000000000000{ "cert_configs": { "macos_keychain": { "issuer": "Google Endpoint Verification", "keychain_type": "system" }, "windows_store": { "issuer": "enterprise_v1_corp_client", "store": "MY", "provider": "current_user" }, "pkcs11": { "slot": "0x1739427", "label": "gecc", "user_pin": "0000", "module": "pkcs11_module.so" } } } enterprise-certificate-proxy-0.3.6/internal/signer/util/util.go000066400000000000000000000047161476410620300247360ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. // Package util provides helper functions for the signer. package util import ( "encoding/json" "io" "os" ) // EnterpriseCertificateConfig contains parameters for initializing signer. type EnterpriseCertificateConfig struct { CertConfigs CertConfigs `json:"cert_configs"` } // CertConfigs is a container for various OS-specific ECP Configs. type CertConfigs struct { MacOSKeychain MacOSKeychain `json:"macos_keychain"` WindowsStore WindowsStore `json:"windows_store"` PKCS11 PKCS11 `json:"pkcs11"` } // MacOSKeychain contains keychain parameters describing the certificate to use. type MacOSKeychain struct { Issuer string `json:"issuer"` KeychainType string `json:"keychain_type"` } // WindowsStore contains Windows key store parameters describing the certificate to use. type WindowsStore struct { Issuer string `json:"issuer"` Store string `json:"store"` Provider string `json:"provider"` } // PKCS11 contains PKCS#11 parameters describing the certificate to use. type PKCS11 struct { Slot string `json:"slot"` // The hexadecimal representation of the uint36 slot ID. (ex:0x1739427) Label string `json:"label"` // The token label (ex: gecc) PKCS11Module string `json:"module"` // The path to the pkcs11 module (shared lib) UserPin string `json:"user_pin"` // Optional user pin to unlock the PKCS #11 module. If it is not defined or empty C_Login will not be called. } // LoadConfig retrieves the ECP config file. func LoadConfig(configFilePath string) (config EnterpriseCertificateConfig, err error) { jsonFile, err := os.Open(configFilePath) if err != nil { return EnterpriseCertificateConfig{}, err } byteValue, err := io.ReadAll(jsonFile) if err != nil { return EnterpriseCertificateConfig{}, err } err = json.Unmarshal(byteValue, &config) if err != nil { return EnterpriseCertificateConfig{}, err } return config, nil } enterprise-certificate-proxy-0.3.6/internal/signer/util/util_test.go000066400000000000000000000047301476410620300257710ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. package util import ( "testing" ) func TestLoadConfig(t *testing.T) { config, err := LoadConfig("./test_data/certificate_config.json") // darwin if err != nil { t.Fatalf("LoadConfig error: %q", err) } want := "Google Endpoint Verification" if config.CertConfigs.MacOSKeychain.Issuer != want { t.Errorf("Expected issuer is %q, got: %q", want, config.CertConfigs.MacOSKeychain.Issuer) } want = "system" if config.CertConfigs.MacOSKeychain.KeychainType != want { t.Errorf("Expected keychain type is %q, got: %q", want, config.CertConfigs.MacOSKeychain.KeychainType) } // windows want = "enterprise_v1_corp_client" if config.CertConfigs.WindowsStore.Issuer != want { t.Errorf("Expected issuer is %q, got: %q", want, config.CertConfigs.WindowsStore.Issuer) } want = "MY" if config.CertConfigs.WindowsStore.Store != want { t.Errorf("Expected store is %q, got: %q", want, config.CertConfigs.WindowsStore.Store) } want = "current_user" if config.CertConfigs.WindowsStore.Provider != want { t.Errorf("Expected provider is %q, got: %q", want, config.CertConfigs.WindowsStore.Provider) } // pkcs11 want = "0x1739427" if config.CertConfigs.PKCS11.Slot != want { t.Errorf("Expected slot is %v, got: %v", want, config.CertConfigs.PKCS11.Slot) } want = "gecc" if config.CertConfigs.PKCS11.Label != want { t.Errorf("Expected label is %v, got: %v", want, config.CertConfigs.PKCS11.Label) } want = "pkcs11_module.so" if config.CertConfigs.PKCS11.PKCS11Module != want { t.Errorf("Expected pkcs11_module is %v, got: %v", want, config.CertConfigs.PKCS11.PKCS11Module) } want = "0000" if config.CertConfigs.PKCS11.UserPin != want { t.Errorf("Expected user pin is %v, got: %v", want, config.CertConfigs.PKCS11.UserPin) } } func TestLoadConfigMissing(t *testing.T) { _, err := LoadConfig("./test_data/certificate_config_missing.json") if err == nil { t.Error("Expected error but got nil") } } enterprise-certificate-proxy-0.3.6/internal/signer/windows/000077500000000000000000000000001476410620300241375ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/windows/.gitattributes000066400000000000000000000000201476410620300270220ustar00rootroot00000000000000*.go text eol=lfenterprise-certificate-proxy-0.3.6/internal/signer/windows/ncrypt/000077500000000000000000000000001476410620300254565ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/internal/signer/windows/ncrypt/cert_util.go000066400000000000000000000255001476410620300300010ustar00rootroot00000000000000// Copyright 2022 Google LLC. // 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 // // https://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. //go:build windows // +build windows // Cert_util provides helpers for working with Windows certificates via crypt32.dll package ncrypt import ( "crypto" "crypto/x509" "errors" "fmt" "io" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( // wincrypt.h constants encodingX509ASN = 1 // X509_ASN_ENCODING certStoreCurrentUserID = 1 // CERT_SYSTEM_STORE_CURRENT_USER_ID certStoreLocalMachineID = 2 // CERT_SYSTEM_STORE_LOCAL_MACHINE_ID infoIssuerFlag = 4 // CERT_INFO_ISSUER_FLAG compareNameStrW = 8 // CERT_COMPARE_NAME_STR_A certStoreProvSystem = 10 // CERT_STORE_PROV_SYSTEM compareShift = 16 // CERT_COMPARE_SHIFT locationShift = 16 // CERT_SYSTEM_STORE_LOCATION_SHIFT findIssuerStr = compareNameStrW<&1 | grep 'CDHash=\(.*\)' | cut -d '=' -f 2) # Need to specify in ACL that the test binary can access the test toolchain # This is because the `-A` param to allow all applications access to the private key on the import # command is apparently not enough... # # This method was found by comparing the diff of `$ security dump-keychain -a` before and after always # allowing the test binary access security set-key-partition-list -S "cdhash:${KEYCHAIN_TEST_BINARY_CD_HASH}" -k ${PASSWORD} ${KEYCHAIN} popd enterprise-certificate-proxy-0.3.6/scripts/softhsm_setup.sh000077500000000000000000000057371476410620300243070ustar00rootroot00000000000000#!/bin/bash # Copyright 2023 Google LLC. # 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 # # https://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. set -eux SOFTHSM2_MODULE="/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so" TOKEN_NAME="Demo Token" OBJECT_LABEL="Demo Object" PIN="0000" install_dependencies() { # Install PKCS #11 related dependencies. # 1. softhsm2 is a software based HSM that implements the PKCS #11 spec. # 2. libp11-kit-dev contains a shared library at we will use to interact with # PKCS #11 device module, as well as pkcs11-tool which will be used for # interacting with the PKCS #11 module. # 3. gnutls-bin contains p11-tool which we will use to create PKCS #11 URIs. sudo apt-get update sudo apt install softhsm2 libp11-kit-dev gnutls-bin opensc } setup_pkcs11_module() { # Make softhsm2 discoverable by PKCS #11 tools. sudo mkdir -p /etc/pkcs11/modules && echo "module: /usr/lib/softhsm/libsofthsm2.so" | sudo tee -a /etc/pkcs11/modules/softhsm.module # Create folder for storing PKCS #11 objects mkdir -p $HOME/.config/softhsm2/tokens cat < $HOME/.config/softhsm2/softhsm2.conf directories.tokendir = $HOME/.config/softhsm2/tokens/ objectstore.backend = file log.level = INFO slots.removable = true EOF pkcs11-tool --init-token --label "$TOKEN_NAME" --module $SOFTHSM2_MODULE --slot 0 --so-pin $PIN SLOT=$(pkcs11-tool --list-slots --module $SOFTHSM2_MODULE | grep -Eo "0x[A-Fa-f0-9]+" | head -n 1) pkcs11-tool --module $SOFTHSM2_MODULE --token-label "$TOKEN_NAME" --login --init-pin --pin $PIN --so-pin $PIN BUILD_DIR=$(mktemp -d) pushd $BUILD_DIR openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj "/C=US/ST=WA/L=Sea/O=My Inc/OU=DevOps/CN=www.example.com/emailAddress=dev@www.example.com" openssl x509 -pubkey -noout -in cert.pem > public_key.pem openssl x509 -in cert.pem -out cert.der -outform der openssl rsa -in key.pem -outform DER -out private_key.der openssl rsa -inform pem -in public_key.pem -outform der -out public_key.der -pubin pkcs11-tool --module $SOFTHSM2_MODULE --slot $SLOT --write-object cert.der --type cert --label "$OBJECT_LABEL" --login --pin $PIN pkcs11-tool --module $SOFTHSM2_MODULE --slot $SLOT --write-object private_key.der --type privkey --label "$OBJECT_LABEL" --login --pin $PIN pkcs11-tool --module $SOFTHSM2_MODULE --slot $SLOT --write-object public_key.der --type pubkey --label "$OBJECT_LABEL" --login --pin $PIN rm -rf $BUILD_DIR popd } if [ $# -eq 0 ]; then install_dependencies setup_pkcs11_module fi enterprise-certificate-proxy-0.3.6/testdata/000077500000000000000000000000001476410620300211535ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/testdata/testcred.p12000066400000000000000000000046551476410620300233260ustar00rootroot000000000000000 0 o *H  ` \0 X0 *H 00 *H 0 *H  0Vy:M!A#ܡI܎(v(n?ܘgBln#T8g#/2b2\zgzqrqtwAE krN 6J,ŋį˱7z\&{FN}~ k}m \ê+s & ~nף/S 8r=ZŎx)FAqZduǒ(*u\ɳ'g*09l_ [oޢy+#qZqR@)j! 9SvOU8UgM\Ʌ0'O d4M]=^!ʩu˻>&!Y%15=i<̶FiסWs0`zjVhEع$u Q"L\ԻjGm>6ItE.zڎ/vBpA _A}ꂯk꽳S95[Cۡ݅ucٰDI_N>]^y&ļǪtyTXuej".՟ry} "+  Zb-/>.AwhDJi(FkD;.}q%Ys={VF?JcI,1\X5aMdZÄߖ+Yšcظݞ, a2Mכϟ xgrj3@3)nwzkҡo.aT'2ݦ2] owPgidjֶ`px:⦳u}ԭU$a^[4l8ܰDCtl5v7{!b7uźu0A *H 2.0*0& *H  00 *H  0j*kUwȕAkyJYv$ uMgLR\&ŕj[Dh'8y(]#<2b^qvb#F^/ 4(J+ZW٦bF L)JEW/ğ7C Z8bJ~[㕶ʶҹuۓq, D$i`džcec쟬daKr>*[p(:2 lyڞn'WSW>hw)o+m mJ\M}DtRAӸX,4~G =f V%1d? -jv=ڕM"Vp"rBDF 4^ 0UFM2/0RZiA:-YL `i;[b8:/0ަ#Mq*ϑt8Ni BV$c4B6mxՊ"8QAC2F>C¤ӑՍ3E25`*`[UIX]m(1 c~ dl+a^MhGI'tY,!A0_[JrۖL]IZɴkUl^=݆JmYKV@Ř4&OF!Rk{jle:lcFRB?1r(!d@=Ǐ+P&Zڔ'НV*ҏc Ѧ}M{fKC7FQ ^D! \)KsʴFI4Xu$RCaHu Keb|9wAì/ʪ?mlN =' P`R 6&j^0?YMsĬlT%դOw*U,Lg"z'҆En$z"~q(=;EyTG~.zsC9%?CZ=pMxhWGƸACi_dٮS~*Z`EƎ%熷SwZ7R:ȒEݤu|:1㳗T{v$GEZټY1mG 9 T~';;~4!GW:v+9[qKY;@}{ rakG8d D*1%0# *H  1a"9lًrA&~q*+010!0 +8z>I EOAb,`enterprise-certificate-proxy-0.3.6/version.txt000066400000000000000000000000071476410620300215650ustar00rootroot00000000000000v0.3.6 enterprise-certificate-proxy-0.3.6/windows/000077500000000000000000000000001476410620300210345ustar00rootroot00000000000000enterprise-certificate-proxy-0.3.6/windows/client.go000066400000000000000000000036771476410620300226560ustar00rootroot00000000000000// Copyright 2023 Google LLC. // 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 // // https://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. // Package windows contains a windows-specific client for accessing the ncrypt APIs directly, // bypassing the RPC-mechanism of the universal client. package windows import ( "crypto" "io" "github.com/googleapis/enterprise-certificate-proxy/internal/signer/windows/ncrypt" ) // SecureKey is a public wrapper for the internal ncrypt implementation. type SecureKey struct { key *ncrypt.Key } // CertificateChain returns the SecureKey's raw X509 cert chain. This contains the public key. func (sk *SecureKey) CertificateChain() [][]byte { return sk.key.CertificateChain() } // Public returns the public key for this SecureKey. func (sk *SecureKey) Public() crypto.PublicKey { return sk.key.Public() } // Sign signs a message digest, using the specified signer options. func (sk *SecureKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signed []byte, err error) { return sk.key.Sign(nil, digest, opts) } // Close frees up resources associated with the underlying key. func (sk *SecureKey) Close() { sk.key.Close() } // NewSecureKey returns a handle to the first available certificate and private key pair in // the specified Windows key store matching the filters. func NewSecureKey(issuer string, store string, provider string) (*SecureKey, error) { k, err := ncrypt.Cred(issuer, store, provider) if err != nil { return nil, err } return &SecureKey{key: k}, nil }