aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Willemsen <dwillemsen@google.com>2023-03-22 18:23:31 -0400
committerDan Willemsen <dwillemsen@google.com>2023-03-22 18:24:15 -0400
commit6f1099df1a49a9efbc715b5e209d29466f67e174 (patch)
treec1a3a05137b7b10a908d54468dca0b4d489cf3c2
parent61a4dd09dab32c56fa1dab3609e7e865eaa78b4d (diff)
parentd0957a96ce28f68cd21ce2742c06237f3fa93fbe (diff)
downloadgoogleapis-enterprise-certificate-proxy-6f1099df1a49a9efbc715b5e209d29466f67e174.tar.gz
Merge tag 'upstream/v0.2.3' into aosp/masterHEADmastermain
Change-Id: I83d53c60d536a3780ab3316968d86185fe80c64c
-rw-r--r--.github/workflows/test-client.yml31
-rw-r--r--.github/workflows/test-cshared.yml25
-rw-r--r--.github/workflows/test-signer-darwin.yml47
-rw-r--r--.github/workflows/test-signer-linux.yml42
-rw-r--r--.github/workflows/test-signer-windows.yml42
-rw-r--r--.gitignore5
-rw-r--r--CODEOWNERS2
-rw-r--r--CODE_OF_CONDUCT.md93
-rw-r--r--CONTRIBUTING.md28
-rw-r--r--LICENSE202
-rw-r--r--METADATA13
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS4
-rw-r--r--README.md155
-rw-r--r--SECURITY.md7
-rwxr-xr-xbuild/scripts/darwin_amd64.sh30
-rwxr-xr-xbuild/scripts/darwin_arm64.sh30
-rwxr-xr-xbuild/scripts/linux_amd64.sh28
-rw-r--r--build/scripts/windows_amd64.ps131
-rw-r--r--client/client.go185
-rw-r--r--client/client_test.go99
-rw-r--r--client/testdata/certificate_config.json10
-rw-r--r--client/testdata/certificate_config_missing_path.json9
-rwxr-xr-xclient/testdata/signer.sh16
-rw-r--r--client/testdata/testcert.pem49
-rw-r--r--client/util/test_data/certificate_config.json6
-rw-r--r--client/util/util.go91
-rw-r--r--client/util/util_test.go29
-rw-r--r--cshared/main.go152
-rw-r--r--go.mod3
-rw-r--r--go.sum0
-rw-r--r--internal/signer/darwin/go.mod3
-rw-r--r--internal/signer/darwin/keychain/keychain.go407
-rw-r--r--internal/signer/darwin/keychain/keychain_test.go48
-rw-r--r--internal/signer/darwin/signer.go132
-rw-r--r--internal/signer/darwin/util/test_data/certificate_config.json8
-rw-r--r--internal/signer/darwin/util/util.go55
-rw-r--r--internal/signer/darwin/util/util_test.go29
-rw-r--r--internal/signer/linux/go.mod5
-rw-r--r--internal/signer/linux/go.sum4
-rw-r--r--internal/signer/linux/signer.go132
-rw-r--r--internal/signer/linux/util/cert_util.go112
-rw-r--r--internal/signer/linux/util/test_data/certificate_config.json10
-rw-r--r--internal/signer/linux/util/util.go70
-rw-r--r--internal/signer/linux/util/util_test.go66
-rw-r--r--internal/signer/test/signer.go110
-rw-r--r--internal/signer/windows/.gitattributes1
-rw-r--r--internal/signer/windows/go.mod8
-rw-r--r--internal/signer/windows/go.sum11
-rw-r--r--internal/signer/windows/ncrypt/cert_util.go300
-rw-r--r--internal/signer/windows/ncrypt/cert_util_test.go32
-rw-r--r--internal/signer/windows/ncrypt/ncrypt.go170
-rw-r--r--internal/signer/windows/signer.go132
-rw-r--r--internal/signer/windows/util/test_data/certificate_config.json9
-rw-r--r--internal/signer/windows/util/util.go57
-rw-r--r--internal/signer/windows/util/util_test.go37
-rw-r--r--renovate.json6
57 files changed, 3418 insertions, 0 deletions
diff --git a/.github/workflows/test-client.yml b/.github/workflows/test-client.yml
new file mode 100644
index 0000000..2a61bac
--- /dev/null
+++ b/.github/workflows/test-client.yml
@@ -0,0 +1,31 @@
+name: Build and Test Client
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+
+ - name: Build
+ run: go build -v ./client/...
+
+ - name: Test
+ run: go test -v ./client/...
+
+ - name: Lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ version: latest
+ args: -E gofmt --max-same-issues 0
diff --git a/.github/workflows/test-cshared.yml b/.github/workflows/test-cshared.yml
new file mode 100644
index 0000000..4a6b041
--- /dev/null
+++ b/.github/workflows/test-cshared.yml
@@ -0,0 +1,25 @@
+name: Build and Test C-Shared Library
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.18
+
+ - name: Build
+ run: go build -buildmode=c-shared -v -o signer.so ./cshared/...
+
+ - name: Test
+ run: go test -v ./cshared/...
diff --git a/.github/workflows/test-signer-darwin.yml b/.github/workflows/test-signer-darwin.yml
new file mode 100644
index 0000000..bcfe6ce
--- /dev/null
+++ b/.github/workflows/test-signer-darwin.yml
@@ -0,0 +1,47 @@
+name: Build and Test Signer Darwin
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ build:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+
+ - name: Build
+ working-directory: ./internal/signer/darwin
+ run: go build -v ./...
+
+ - name: Test
+ working-directory: ./internal/signer/darwin
+ run: go test -v ./...
+
+ - name: Lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ version: latest
+ working-directory: ./internal/signer/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@v3
+ with:
+ name: darwin_amd64
+ path: ./build/bin/darwin_amd64/*
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: darwin_arm64
+ path: ./build/bin/darwin_arm64/*
diff --git a/.github/workflows/test-signer-linux.yml b/.github/workflows/test-signer-linux.yml
new file mode 100644
index 0000000..1949146
--- /dev/null
+++ b/.github/workflows/test-signer-linux.yml
@@ -0,0 +1,42 @@
+name: Build and Test Signer Linux
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+
+ - name: Build
+ working-directory: ./internal/signer/linux
+ run: go build -v ./...
+
+ - name: Test
+ working-directory: ./internal/signer/linux
+ run: go test -v ./...
+
+ - name: Lint
+ uses: golangci/golangci-lint-action@v3
+ 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@v3
+ with:
+ name: linux_amd64
+ path: ./build/bin/linux_amd64/*
diff --git a/.github/workflows/test-signer-windows.yml b/.github/workflows/test-signer-windows.yml
new file mode 100644
index 0000000..7bd1c3d
--- /dev/null
+++ b/.github/workflows/test-signer-windows.yml
@@ -0,0 +1,42 @@
+name: Build and Test Signer Windows
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ build:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+
+ - 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@v3
+ 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@v3
+ with:
+ name: windows_amd64
+ path: .\build\bin\windows_amd64\*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..615006f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# MacOS
+.DS_Store
+
+# compiled binaries
+build/bin/**
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..d3e5d69
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,2 @@
+# Default owner for all directories not owned by others
+* @andyrzhao @shinfan @clundin25
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..dc079b4
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,93 @@
+# 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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6272489
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# 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 <https://cla.developers.google.com/> 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/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ 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.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..c6a8c22
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,13 @@
+name: "Google Proxies for Enterprise Certificates"
+description:
+ "Repository for the Enterprise Certificate Proxy project."
+
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/googleapis/enterprise-certificate-proxy"
+ }
+ version: "v0.2.3"
+ last_upgrade_date { year: 2023 month: 3 day: 22 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..edf3072
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,4 @@
+bili@google.com
+dwillemsen@google.com
+ryantao@google.com
+wenjiah@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..45ba449
--- /dev/null
+++ b/README.md
@@ -0,0 +1,155 @@
+# Google Proxies for Enterprise Certificates (Preview)
+
+## 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
+
+Currently ECP is in Preview stage and all the APIs and configurations are **subject to change**.
+
+The following platforms/keystores are supported by ECP:
+
+- MacOS: __Keychain__
+- Linux: __PKCS#11__
+- Windows: __MY__
+
+## Prerequisites
+
+Before using ECP with your application/client, you should follow the instructions [here][enterprisecert] to configure your enterprise certificate policies with Access Context Manager.
+
+### Quick Start
+
+1. Install gcloud CLI (Cloud SDK) at: https://cloud.google.com/sdk/docs/install.
+
+ 1. **Note:** gcloud version 416.0 or newer is required.
+
+1. `$ gcloud components install enterprise-certificate-proxy`.
+
+1. **MacOS ONLY**
+
+ 1. `$ gcloud config virtualenv create`
+
+ 1. `$ gcloud config virtualenv enable`
+
+1. Create a new JSON file at `~/.config/gcloud/certificate_config.json`:
+
+ - Alternatively you can put the JSON in the location of your choice and set the path to it using:
+
+ `$ gcloud config set context_aware/enterprise_certificate_config_file_path "<json file path>"`.
+
+ - Another approach for setting the JSON file location is setting the location with the `GOOGLE_API_CERTIFICATE_CONFIG` environment variable.
+
+1. Update the `certificate_config.json` file with details about the certificate (See [Configuration](#certificate-configutation) section for details.)
+
+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 GCP resources with mTLS.
+
+### Certificate Configuration
+
+ECP relies on the `certificate_config.json` file to read all the metadata information for locating the certificate. The contents of this JSON file look like the following:
+
+#### 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.
+```
+
+## Build binaries
+
+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/access-context-manager/docs/enterprise-certificates
+[zerotrust]: https://cloud.google.com/blog/topics/developers-practitioners/zero-trust-and-beyondcorp-google-cloud
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..8b58ae9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,7 @@
+# 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.
diff --git a/build/scripts/darwin_amd64.sh b/build/scripts/darwin_amd64.sh
new file mode 100755
index 0000000..baec3b1
--- /dev/null
+++ b/build/scripts/darwin_amd64.sh
@@ -0,0 +1,30 @@
+#!/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 -eu
+
+# 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 signer ./../../../build/bin/darwin_amd64/ecp
+cd ./../../..
+
+# Build the signer library
+go build -buildmode=c-shared -o build/bin/darwin_amd64/libecp.dylib cshared/main.go
+rm build/bin/darwin_amd64/libecp.h
diff --git a/build/scripts/darwin_arm64.sh b/build/scripts/darwin_arm64.sh
new file mode 100755
index 0000000..efc6008
--- /dev/null
+++ b/build/scripts/darwin_arm64.sh
@@ -0,0 +1,30 @@
+#!/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 -eu
+
+# 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 signer ./../../../build/bin/darwin_arm64/ecp
+cd ./../../..
+
+# Build the signer library
+CGO_ENABLED=1 GO111MODULE=on GOARCH=arm64 go build -buildmode=c-shared -o build/bin/darwin_arm64/libecp.dylib cshared/main.go
+rm build/bin/darwin_arm64/libecp.h
diff --git a/build/scripts/linux_amd64.sh b/build/scripts/linux_amd64.sh
new file mode 100755
index 0000000..36bcb67
--- /dev/null
+++ b/build/scripts/linux_amd64.sh
@@ -0,0 +1,28 @@
+#!/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.
+
+# 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 -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 signer ./../../../build/bin/linux_amd64/ecp
+cd ./../../..
diff --git a/build/scripts/windows_amd64.ps1 b/build/scripts/windows_amd64.ps1
new file mode 100644
index 0000000..d259535
--- /dev/null
+++ b/build/scripts/windows_amd64.ps1
@@ -0,0 +1,31 @@
+# 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 .\signer.exe ..\..\..\build\bin\windows_amd64\ecp.exe
+Set-Location ..\..\..\
+
+# Build the signer library
+go build -buildmode=c-shared -o .\build\bin\windows_amd64\libecp.dll .\cshared\main.go
+Remove-Item .\build\bin\windows_amd64\libecp.h
diff --git a/client/client.go b/client/client.go
new file mode 100644
index 0000000..b3283b8
--- /dev/null
+++ b/client/client.go
@@ -0,0 +1,185 @@
+// 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"
+
+// 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(&rsa.PSSOptions{})
+}
+
+// SignArgs contains arguments to a crypto Signer.Sign method.
+type SignArgs struct {
+ Digest []byte // The content to sign.
+ Opts crypto.SignerOpts // Options for signing, such as Hash identifier.
+}
+
+// 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 options.
+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
+}
+
+// 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 == "" {
+ 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
+}
diff --git a/client/client_test.go b/client/client_test.go
new file mode 100644
index 0000000..b0305b4
--- /dev/null
+++ b/client/client_test.go
@@ -0,0 +1,99 @@
+// 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"
+ "errors"
+ "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_PathMissing(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_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 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)
+ }
+}
diff --git a/client/testdata/certificate_config.json b/client/testdata/certificate_config.json
new file mode 100644
index 0000000..89523d4
--- /dev/null
+++ b/client/testdata/certificate_config.json
@@ -0,0 +1,10 @@
+{
+ "cert_configs": {
+ "test": {
+ "issuer": "Test Issuer"
+ }
+ },
+ "libs": {
+ "ecp": "./testdata/signer.sh"
+ }
+}
diff --git a/client/testdata/certificate_config_missing_path.json b/client/testdata/certificate_config_missing_path.json
new file mode 100644
index 0000000..327d54c
--- /dev/null
+++ b/client/testdata/certificate_config_missing_path.json
@@ -0,0 +1,9 @@
+{
+ "cert_configs": {
+ "test": {
+ "issuer": "Test Issuer"
+ }
+ },
+ "libs": {
+ }
+}
diff --git a/client/testdata/signer.sh b/client/testdata/signer.sh
new file mode 100755
index 0000000..959ecf2
--- /dev/null
+++ b/client/testdata/signer.sh
@@ -0,0 +1,16 @@
+#!/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.pem \ No newline at end of file
diff --git a/client/testdata/testcert.pem b/client/testdata/testcert.pem
new file mode 100644
index 0000000..f33fc52
--- /dev/null
+++ b/client/testdata/testcert.pem
@@ -0,0 +1,49 @@
+-----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-----
diff --git a/client/util/test_data/certificate_config.json b/client/util/test_data/certificate_config.json
new file mode 100644
index 0000000..ed9d485
--- /dev/null
+++ b/client/util/test_data/certificate_config.json
@@ -0,0 +1,6 @@
+{
+ "libs": {
+ "ecp": "C:/Program Files (x86)/Google/Endpoint Verification/signer.exe"
+ }
+}
+
diff --git a/client/util/util.go b/client/util/util.go
new file mode 100644
index 0000000..1640ec1
--- /dev/null
+++ b/client/util/util.go
@@ -0,0 +1,91 @@
+// 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"
+)
+
+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
+ }
+ 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)
+}
diff --git a/client/util/util_test.go b/client/util/util_test.go
new file mode 100644
index 0000000..2066fcd
--- /dev/null
+++ b/client/util/util_test.go
@@ -0,0 +1,29 @@
+// 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 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)
+ }
+}
diff --git a/cshared/main.go b/cshared/main.go
new file mode 100644
index 0000000..927719f
--- /dev/null
+++ b/cshared/main.go
@@ -0,0 +1,152 @@
+// 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 <stdlib.h>
+*/
+import "C"
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "encoding/pem"
+ "io"
+ "log"
+ "os"
+ "unsafe"
+
+ "github.com/googleapis/enterprise-certificate-proxy/client"
+)
+
+// 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
+}
+
+// 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.
+//
+//export GetCertPemForPython
+func GetCertPemForPython(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)
+}
+
+// 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.
+//
+//export SignForPython
+func SignForPython(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)
+}
+
+func main() {}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..def0549
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/googleapis/enterprise-certificate-proxy
+
+go 1.19
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/go.sum
diff --git a/internal/signer/darwin/go.mod b/internal/signer/darwin/go.mod
new file mode 100644
index 0000000..5f52caa
--- /dev/null
+++ b/internal/signer/darwin/go.mod
@@ -0,0 +1,3 @@
+module signer
+
+go 1.19 \ No newline at end of file
diff --git a/internal/signer/darwin/keychain/keychain.go b/internal/signer/darwin/keychain/keychain.go
new file mode 100644
index 0000000..6759904
--- /dev/null
+++ b/internal/signer/darwin/keychain/keychain.go
@@ -0,0 +1,407 @@
+// 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 <CoreFoundation/CoreFoundation.h>
+#include <Security/Security.h>
+*/
+import "C"
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "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,
+ }
+ 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,
+ }
+)
+
+// 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) *cfError {
+ 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
+}
+
+// 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) (*Key, error) {
+ k := &Key{
+ privateKeyRef: privateKeyRef,
+ certs: certs,
+ }
+
+ // 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))
+ 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))
+ })
+ 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
+}
+
+// Cred gets the first Credential (filtering on issuer) corresponding to
+// available certificate and private key pairs (i.e. identities) available in
+// the Keychain. This includes both the current login keychain for the user,
+// and the system keychain.
+func Cred(issuerCN string) (*Key, 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))
+ // Do the matching-item copy.
+ var leafMatches C.CFTypeRef
+ if errno := C.SecItemCopyMatching((C.CFDictionaryRef)(leafSearch), &leafMatches); errno != C.errSecSuccess {
+ return nil, keychainError(errno)
+ }
+ defer C.CFRelease(leafMatches)
+ signingIdents := C.CFArrayRef(leafMatches)
+ // Dump the certs into golang x509 Certificates.
+ var (
+ leafIdent C.SecIdentityRef
+ leaf *x509.Certificate
+ )
+ // Find the first valid leaf whose issuer (CA) matches the name in filter.
+ // Validation in identityToX509 covers Not Before, Not After and key alg.
+ for i := 0; i < int(C.CFArrayGetCount(signingIdents)) && leaf == nil; i++ {
+ identDict := C.CFArrayGetValueAtIndex(signingIdents, C.CFIndex(i))
+ xc, err := identityToX509(C.SecIdentityRef(identDict))
+ if err != nil {
+ continue
+ }
+ if xc.Issuer.CommonName == issuerCN {
+ leaf = xc
+ leafIdent = C.SecIdentityRef(identDict)
+ }
+ }
+
+ 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 := identityToSecKeyRef(leafIdent)
+ if err != nil {
+ return nil, err
+ }
+ defer C.CFRelease(C.CFTypeRef(skr))
+ return newKey(skr, certs)
+}
+
+// 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 identityToSecKeyRef(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 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
+}
diff --git a/internal/signer/darwin/keychain/keychain_test.go b/internal/signer/darwin/keychain/keychain_test.go
new file mode 100644
index 0000000..946ba9b
--- /dev/null
+++ b/internal/signer/darwin/keychain/keychain_test.go
@@ -0,0 +1,48 @@
+// 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"
+ "testing"
+ "unsafe"
+)
+
+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)
+ }
+}
diff --git a/internal/signer/darwin/signer.go b/internal/signer/darwin/signer.go
new file mode 100644
index 0000000..3eac7db
--- /dev/null
+++ b/internal/signer/darwin/signer.go
@@ -0,0 +1,132 @@
+// 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"
+ "signer/keychain"
+ "signer/util"
+ "time"
+)
+
+// 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{})
+}
+
+// SignArgs contains arguments to a crypto Signer.Sign method.
+type SignArgs struct {
+ Digest []byte // The content to sign.
+ Opts crypto.SignerOpts // Options for signing, such as Hash identifier.
+}
+
+// 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.
+func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) {
+ *resp, err = k.key.Sign(nil, args.Digest, 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)
+ 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})
+}
diff --git a/internal/signer/darwin/util/test_data/certificate_config.json b/internal/signer/darwin/util/test_data/certificate_config.json
new file mode 100644
index 0000000..a4f0edf
--- /dev/null
+++ b/internal/signer/darwin/util/test_data/certificate_config.json
@@ -0,0 +1,8 @@
+{
+ "cert_configs": {
+ "macos_keychain": {
+ "issuer": "Google Endpoint Verification"
+ }
+ }
+}
+
diff --git a/internal/signer/darwin/util/util.go b/internal/signer/darwin/util/util.go
new file mode 100644
index 0000000..b8019d8
--- /dev/null
+++ b/internal/signer/darwin/util/util.go
@@ -0,0 +1,55 @@
+// 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 ECP Configs.
+type CertConfigs struct {
+ MacOSKeychain MacOSKeychain `json:"macos_keychain"`
+}
+
+// MacOSKeychain contains parameters describing the certificate to use.
+type MacOSKeychain struct {
+ Issuer string `json:"issuer"`
+}
+
+// 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
+
+}
diff --git a/internal/signer/darwin/util/util_test.go b/internal/signer/darwin/util/util_test.go
new file mode 100644
index 0000000..372ef7e
--- /dev/null
+++ b/internal/signer/darwin/util/util_test.go
@@ -0,0 +1,29 @@
+// 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")
+ if err != nil {
+ t.Errorf("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)
+ }
+}
diff --git a/internal/signer/linux/go.mod b/internal/signer/linux/go.mod
new file mode 100644
index 0000000..96aab0c
--- /dev/null
+++ b/internal/signer/linux/go.mod
@@ -0,0 +1,5 @@
+module signer
+
+go 1.19
+
+require github.com/google/go-pkcs11 v0.2.0
diff --git a/internal/signer/linux/go.sum b/internal/signer/linux/go.sum
new file mode 100644
index 0000000..d01e7f0
--- /dev/null
+++ b/internal/signer/linux/go.sum
@@ -0,0 +1,4 @@
+github.com/google/go-pkcs11 v0.1.1-0.20220804004530-aced8594bb2e h1:y7UBq7yC0nK2b4h9uisyrhYVd21Ju/2GyzRve8dOvtk=
+github.com/google/go-pkcs11 v0.1.1-0.20220804004530-aced8594bb2e/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
+github.com/google/go-pkcs11 v0.2.0 h1:5meDPB26aJ98f+K9G21f0AqZwo/S5BJMJh8nuhMbdsI=
+github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
diff --git a/internal/signer/linux/signer.go b/internal/signer/linux/signer.go
new file mode 100644
index 0000000..ac2bb25
--- /dev/null
+++ b/internal/signer/linux/signer.go
@@ -0,0 +1,132 @@
+// 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"
+ "signer/util"
+ "time"
+)
+
+// 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{})
+}
+
+// SignArgs contains arguments to a crypto Signer.Sign method.
+type SignArgs struct {
+ Digest []byte // The content to sign.
+ Opts crypto.SignerOpts // Options for signing, such as Hash identifier.
+}
+
+// A EnterpriseCertSigner exports RPC methods for signing.
+type EnterpriseCertSigner struct {
+ key *util.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.
+func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) {
+ *resp, err = k.key.Sign(nil, args.Digest, 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 = util.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})
+}
diff --git a/internal/signer/linux/util/cert_util.go b/internal/signer/linux/util/cert_util.go
new file mode 100644
index 0000000..07a1449
--- /dev/null
+++ b/internal/signer/linux/util/cert_util.go
@@ -0,0 +1,112 @@
+// 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.
+
+// Cert_util provides helpers for working with certificates via PKCS11
+package util
+
+import (
+ "crypto"
+ "errors"
+ "io"
+
+ "github.com/google/go-pkcs11/pkcs11"
+)
+
+// 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
+ }
+ 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
+ }
+ 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
+ }
+ 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")
+ }
+
+ return &Key{
+ slot: kslot,
+ signer: ksigner,
+ chain: kchain,
+ }, 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
+}
+
+// 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()
+}
+
+// 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)
+}
diff --git a/internal/signer/linux/util/test_data/certificate_config.json b/internal/signer/linux/util/test_data/certificate_config.json
new file mode 100644
index 0000000..64ed1c2
--- /dev/null
+++ b/internal/signer/linux/util/test_data/certificate_config.json
@@ -0,0 +1,10 @@
+{
+ "cert_configs": {
+ "pkcs11": {
+ "slot": "0x1739427",
+ "label": "gecc",
+ "user_pin": "0000",
+ "module": "pkcs11_module.so"
+ }
+ }
+}
diff --git a/internal/signer/linux/util/util.go b/internal/signer/linux/util/util.go
new file mode 100644
index 0000000..630840a
--- /dev/null
+++ b/internal/signer/linux/util/util.go
@@ -0,0 +1,70 @@
+// 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"
+ "strconv"
+ "strings"
+)
+
+// 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
+}
+
+// EnterpriseCertificateConfig contains parameters for initializing signer.
+type EnterpriseCertificateConfig struct {
+ CertConfigs CertConfigs `json:"cert_configs"`
+}
+
+// CertConfigs is a container for various ECP Configs.
+type CertConfigs struct {
+ PKCS11 PKCS11 `json:"pkcs11"`
+}
+
+// PKCS11 contains 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
+
+}
diff --git a/internal/signer/linux/util/util_test.go b/internal/signer/linux/util/util_test.go
new file mode 100644
index 0000000..86f2b64
--- /dev/null
+++ b/internal/signer/linux/util/util_test.go
@@ -0,0 +1,66 @@
+// 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")
+ if err != nil {
+ t.Fatalf("LoadConfig error: %v", err)
+ }
+ 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")
+ }
+}
+
+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")
+ }
+}
diff --git a/internal/signer/test/signer.go b/internal/signer/test/signer.go
new file mode 100644
index 0000000..c34fc14
--- /dev/null
+++ b/internal/signer/test/signer.go
@@ -0,0 +1,110 @@
+// 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
+}
+
+// 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.
+func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) {
+ *resp = args.Digest
+ 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})
+}
diff --git a/internal/signer/windows/.gitattributes b/internal/signer/windows/.gitattributes
new file mode 100644
index 0000000..a0717e4
--- /dev/null
+++ b/internal/signer/windows/.gitattributes
@@ -0,0 +1 @@
+*.go text eol=lf \ No newline at end of file
diff --git a/internal/signer/windows/go.mod b/internal/signer/windows/go.mod
new file mode 100644
index 0000000..b6b5b16
--- /dev/null
+++ b/internal/signer/windows/go.mod
@@ -0,0 +1,8 @@
+module signer
+
+go 1.19
+
+require (
+ golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
+ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
+)
diff --git a/internal/signer/windows/go.sum b/internal/signer/windows/go.sum
new file mode 100644
index 0000000..c085ca2
--- /dev/null
+++ b/internal/signer/windows/go.sum
@@ -0,0 +1,11 @@
+golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
+golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/internal/signer/windows/ncrypt/cert_util.go b/internal/signer/windows/ncrypt/cert_util.go
new file mode 100644
index 0000000..f2f078a
--- /dev/null
+++ b/internal/signer/windows/ncrypt/cert_util.go
@@ -0,0 +1,300 @@
+// 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<<compareShift | infoIssuerFlag // CERT_FIND_ISSUER_STR_W
+ certStoreLocalMachine = certStoreLocalMachineID << locationShift // CERT_SYSTEM_STORE_LOCAL_MACHINE
+ certStoreCurrentUser = certStoreCurrentUserID << locationShift // CERT_SYSTEM_STORE_CURRENT_USER
+ signatureKeyUsage = 0x80 // CERT_DIGITAL_SIGNATURE_KEY_USAGE
+ acquireCached = 0x1 // CRYPT_ACQUIRE_CACHE_FLAG
+ acquireSilent = 0x40 // CRYPT_ACQUIRE_SILENT_FLAG
+ acquireOnlyNCryptKey = 0x40000 // CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG
+ ncryptKeySpec = 0xFFFFFFFF // CERT_NCRYPT_KEY_SPEC
+ certChainCacheOnlyURLRetrieval = 0x00000004 // CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL
+ certChainDisableAIA = 0x00002000 // CERT_CHAIN_DISABLE_AIA
+ certChainRevocationCheckCacheOnly = 0x80000000 // CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY
+
+ hcceLocalMachine = windows.Handle(0x01) // HCCE_LOCAL_MACHINE
+
+ // winerror.h constants
+ cryptENotFound = 0x80092004 // CRYPT_E_NOT_FOUND
+)
+
+var (
+ null = uintptr(unsafe.Pointer(nil))
+
+ crypt32 = windows.MustLoadDLL("crypt32.dll")
+
+ certFindCertificateInStore = crypt32.MustFindProc("CertFindCertificateInStore")
+ certGetIntendedKeyUsage = crypt32.MustFindProc("CertGetIntendedKeyUsage")
+ cryptAcquireCertificatePrivateKey = crypt32.MustFindProc("CryptAcquireCertificatePrivateKey")
+)
+
+// findCert wraps the CertFindCertificateInStore call. Note that any cert context passed
+// into prev will be freed. If no certificate was found, nil will be returned.
+func findCert(store windows.Handle, enc uint32, findFlags uint32, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) {
+ h, _, err := certFindCertificateInStore.Call(
+ uintptr(store),
+ uintptr(enc),
+ uintptr(findFlags),
+ uintptr(findType),
+ uintptr(unsafe.Pointer(para)),
+ uintptr(unsafe.Pointer(prev)),
+ )
+ if h == 0 {
+ // Actual error, or simply not found?
+ errno, ok := err.(syscall.Errno)
+ if !ok {
+ return nil, err
+ }
+ if errno == cryptENotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return (*windows.CertContext)(unsafe.Pointer(h)), nil
+}
+
+// extractSimpleChain extracts the final certificate chain from a CertSimpleChain.
+// Adapted from crypto.x509.root_windows
+func extractSimpleChain(simpleChain **windows.CertSimpleChain, chainCount int) ([]*x509.Certificate, error) {
+ if simpleChain == nil || chainCount == 0 {
+ return nil, errors.New("invalid simple chain")
+ }
+ // Convert the simpleChain array to a huge slice and slice it to the length we want.
+ // https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices
+ simpleChains := (*[1 << 20]*windows.CertSimpleChain)(unsafe.Pointer(simpleChain))[:chainCount:chainCount]
+ // Each simple chain contains the chain of certificates, summary trust information
+ // about the chain, and trust information about each certificate element in the chain.
+ // Select the last chain since only expect to encounter one chain.
+ lastChain := simpleChains[chainCount-1]
+ chainLen := int(lastChain.NumElements)
+ elements := (*[1 << 20]*windows.CertChainElement)(unsafe.Pointer(lastChain.Elements))[:chainLen:chainLen]
+ chain := make([]*x509.Certificate, 0, chainLen)
+ for _, element := range elements {
+ xc, err := certContextToX509(element.CertContext)
+ if err != nil {
+ return nil, err
+ }
+ chain = append(chain, xc)
+ }
+ return chain, nil
+}
+
+// findCertChain builds a chain from a given certificate using the local machine store.
+func findCertChain(cert *windows.CertContext) ([]*x509.Certificate, error) {
+ var (
+ chainPara windows.CertChainPara
+ chainCtx *windows.CertChainContext
+ )
+
+ // Search the system for candidate certificate chains.
+ // Because we are using unsafe pointers here, we CANNOT directly call
+ // CertGetCertificateChain and MUST either use the windows or syscall library
+ // to validly use unsafe pointers.
+ // See https://golang.org/pkg/unsafe/#Pointer for valid unsafe package patterns.
+ chainPara.Size = uint32(unsafe.Sizeof(chainPara))
+ err := windows.CertGetCertificateChain(
+ hcceLocalMachine,
+ cert,
+ nil,
+ cert.Store,
+ &chainPara,
+ certChainRevocationCheckCacheOnly|certChainCacheOnlyURLRetrieval|certChainDisableAIA,
+ 0,
+ &chainCtx)
+
+ if err != nil {
+ return nil, fmt.Errorf("getCertificateChain: %w", err)
+ }
+ defer windows.CertFreeCertificateChain(chainCtx)
+
+ x509Certs, err := extractSimpleChain(chainCtx.Chains, int(chainCtx.ChainCount))
+ if err != nil {
+ return nil, fmt.Errorf("getCertificateChain extractSimpleChain: %w", err)
+ }
+ return x509Certs, nil
+}
+
+// intendedKeyUsage wraps CertGetIntendedKeyUsage. If there are key usage bytes they will be returned,
+// otherwise 0 will be returned.
+func intendedKeyUsage(enc uint32, cert *windows.CertContext) (usage uint16) {
+ _, _, _ = certGetIntendedKeyUsage.Call(uintptr(enc), uintptr(unsafe.Pointer(cert.CertInfo)), uintptr(unsafe.Pointer(&usage)), 2)
+ return
+}
+
+// acquirePrivateKey wraps CryptAcquireCertificatePrivateKey.
+func acquirePrivateKey(cert *windows.CertContext) (windows.Handle, error) {
+ var (
+ key windows.Handle
+ keySpec uint32
+ mustFree int
+ )
+ r, _, err := cryptAcquireCertificatePrivateKey.Call(
+ uintptr(unsafe.Pointer(cert)),
+ acquireCached|acquireSilent|acquireOnlyNCryptKey,
+ null,
+ uintptr(unsafe.Pointer(&key)),
+ uintptr(unsafe.Pointer(&keySpec)),
+ uintptr(unsafe.Pointer(&mustFree)),
+ )
+ if r == 0 {
+ return 0, fmt.Errorf("acquiring private key: %x %w", r, err)
+ }
+ if mustFree != 0 {
+ return 0, fmt.Errorf("wrong mustFree [%d != 0]", mustFree)
+ }
+ if keySpec != ncryptKeySpec {
+ return 0, fmt.Errorf("wrong keySpec [%d != %d]", keySpec, ncryptKeySpec)
+ }
+ return key, nil
+}
+
+// certContextToX509 extracts the x509 certificate from the cert context.
+func certContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
+ // To ensure we don't mess with the cert context's memory, use a copy of it.
+ src := (*[1 << 20]byte)(unsafe.Pointer(ctx.EncodedCert))[:ctx.Length:ctx.Length]
+ der := make([]byte, int(ctx.Length))
+ copy(der, src)
+
+ xc, err := x509.ParseCertificate(der)
+ if err != nil {
+ return xc, err
+ }
+ return xc, nil
+}
+
+// Cred returns a Key wrapping the first valid certificate in the system store
+// matching a given issuer string.
+func Cred(issuer string, storeName string, provider string) (*Key, error) {
+ var certStore uint32
+ if provider == "local_machine" {
+ certStore = uint32(certStoreLocalMachine)
+ } else if provider == "current_user" {
+ certStore = uint32(certStoreCurrentUser)
+ } else {
+ return nil, errors.New("provider must be local_machine or current_user")
+ }
+ storeNamePtr, err := windows.UTF16PtrFromString(storeName)
+ if err != nil {
+ return nil, err
+ }
+ store, err := windows.CertOpenStore(certStoreProvSystem, 0, null, certStore, uintptr(unsafe.Pointer(storeNamePtr)))
+ if err != nil {
+ return nil, fmt.Errorf("opening certificate store: %w", err)
+ }
+ i, err := windows.UTF16PtrFromString(issuer)
+ if err != nil {
+ return nil, err
+ }
+ var prev *windows.CertContext
+ for {
+ nc, err := findCert(store, encodingX509ASN, 0, findIssuerStr, i, prev)
+ if err != nil {
+ return nil, fmt.Errorf("finding certificates: %w", err)
+ }
+ if nc == nil {
+ return nil, errors.New("no certificate found")
+ }
+ prev = nc
+ if (intendedKeyUsage(encodingX509ASN, nc) & signatureKeyUsage) == 0 {
+ continue
+ }
+
+ xc, err := certContextToX509(nc)
+ if err != nil {
+ continue
+ }
+
+ machineChain, err := findCertChain(nc)
+ if err != nil {
+ continue
+ }
+ return &Key{
+ cert: xc,
+ ctx: nc,
+ store: store,
+ chain: machineChain,
+ }, nil
+ }
+}
+
+// Key is a wrapper around the certificate store and context that uses it to
+// implement signing-related methods with CryptoNG functionality.
+type Key struct {
+ cert *x509.Certificate
+ ctx *windows.CertContext
+ store windows.Handle
+ chain []*x509.Certificate
+}
+
+// CertificateChain returns the credential as a raw X509 cert chain. This
+// contains the public key.
+func (k *Key) CertificateChain() [][]byte {
+ // Convert the certificates to a list of encoded certificate bytes.
+ chain := make([][]byte, len(k.chain))
+ for i, xc := range k.chain {
+ chain[i] = xc.Raw
+ }
+ return chain
+}
+
+// Close releases resources held by the credential.
+func (k *Key) Close() error {
+ if err := windows.CertFreeCertificateContext(k.ctx); err != nil {
+ return err
+ }
+ return windows.CertCloseStore(k.store, 0)
+}
+
+// Public returns the corresponding public key for this Key.
+func (k *Key) Public() crypto.PublicKey {
+ return k.cert.PublicKey
+}
+
+// Sign signs a message digest. Here, we pass off the signing to the Windows CryptoNG library.
+func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ key, err := acquirePrivateKey(k.ctx)
+ if err != nil {
+ return nil, fmt.Errorf("cannot acquire private key handle: %w", err)
+ }
+ return SignHash(key, k.Public(), digest, opts)
+}
diff --git a/internal/signer/windows/ncrypt/cert_util_test.go b/internal/signer/windows/ncrypt/cert_util_test.go
new file mode 100644
index 0000000..96ef40a
--- /dev/null
+++ b/internal/signer/windows/ncrypt/cert_util_test.go
@@ -0,0 +1,32 @@
+// 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
+
+package ncrypt
+
+import (
+ "testing"
+)
+
+func TestCredProviderNotSupported(t *testing.T) {
+ _, err := Cred("issuer", "store", "unsupported_provider")
+ if err == nil {
+ t.Errorf("Expected error, but got nil.")
+ }
+ want := "provider must be local_machine or current_user"
+ if err.Error() != want {
+ t.Errorf("Expected error is %q, got: %q", want, err.Error())
+ }
+}
diff --git a/internal/signer/windows/ncrypt/ncrypt.go b/internal/signer/windows/ncrypt/ncrypt.go
new file mode 100644
index 0000000..e8997d2
--- /dev/null
+++ b/internal/signer/windows/ncrypt/ncrypt.go
@@ -0,0 +1,170 @@
+// 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
+
+// Package ncrypt provides wrappers around ncrypt.h functions.
+// https://docs.microsoft.com/en-us/windows/win32/api/ncrypt/
+package ncrypt
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "fmt"
+ "math/big"
+ "unsafe"
+
+ "golang.org/x/crypto/cryptobyte"
+ "golang.org/x/crypto/cryptobyte/asn1"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ // bcrypt.h constants
+ bcryptPadPKCS1 = 0x00000002 // BCRYPT_PAD_PKCS1
+ bcryptPadPSS = 0x00000008 // BCRYPT_PAD_PSS
+
+ // ncrypt.h constants
+ nCryptSilentFlag = 0x00000040 // NCRYPT_SILENT_FLAG
+)
+
+var (
+ nCrypt = windows.MustLoadDLL("ncrypt.dll")
+ nCryptSignHash = nCrypt.MustFindProc("NCryptSignHash")
+)
+
+// bcypt.h structs.
+type pkcs1PaddingInfo struct {
+ algID *uint16
+}
+type pssPaddingInfo struct {
+ algID *uint16
+ saltLength uint32
+}
+
+func algID(hashFunc crypto.Hash) (*uint16, bool) {
+ algID, ok := map[crypto.Hash][]uint16{
+ crypto.SHA256: {'S', 'H', 'A', '2', '5', '6', 0}, // BCRYPT_SHA256_ALGORITHM
+ }[hashFunc]
+ return &algID[0], ok
+}
+
+func rsaPadding(opts crypto.SignerOpts, flags *int) (paddingInfo unsafe.Pointer, err error) {
+ if o, ok := opts.(*rsa.PSSOptions); ok {
+ algID, ok := algID(o.HashFunc())
+ if !ok {
+ err = fmt.Errorf("unsupported hash function %T", o.HashFunc())
+ return
+ }
+ saltLength := o.SaltLength
+ switch saltLength {
+ case rsa.PSSSaltLengthAuto:
+ err = fmt.Errorf("rsa.PSSSaltLengthAuto is not supported")
+ return
+ case rsa.PSSSaltLengthEqualsHash:
+ saltLength = o.HashFunc().Size()
+ }
+ paddingInfo = unsafe.Pointer(&pssPaddingInfo{
+ algID: algID,
+ saltLength: uint32(saltLength),
+ })
+ *flags |= bcryptPadPSS
+ return
+ }
+
+ algID, ok := algID(opts.HashFunc())
+ if !ok {
+ err = fmt.Errorf("unsupported hash function %T", opts.HashFunc())
+ return
+ }
+ paddingInfo = unsafe.Pointer(&pkcs1PaddingInfo{
+ algID: algID,
+ })
+ *flags |= bcryptPadPKCS1
+ return
+}
+
+func signHashInternal(priv windows.Handle, pub crypto.PublicKey, digest []byte, flags int, paddingInfo unsafe.Pointer) ([]byte, error) {
+ var size uint32
+ r, _, _ := nCryptSignHash.Call(
+ /* hKey */ uintptr(priv),
+ /* *pPaddingInfo */ uintptr(paddingInfo),
+ /* pbHashValue */ uintptr(unsafe.Pointer(&digest[0])),
+ /* cbHashValue */ uintptr(len(digest)),
+ /* pbSignature */ 0,
+ /* cbSignature */ 0,
+ /* *pcbResult */ uintptr(unsafe.Pointer(&size)),
+ /* dwFlagss */ uintptr(flags))
+ if r != 0 {
+ return nil, fmt.Errorf("NCryptSignHash: failed to get signature length: %#x", r)
+ }
+
+ sig := make([]byte, size)
+ r, _, _ = nCryptSignHash.Call(
+ /* hKey */ uintptr(priv),
+ /* *pPaddingInfo */ uintptr(paddingInfo),
+ /* pbHashValue */ uintptr(unsafe.Pointer(&digest[0])),
+ /* cbHashValue */ uintptr(len(digest)),
+ /* pbSignature */ uintptr(unsafe.Pointer(&sig[0])),
+ /* cbSignature */ uintptr(size),
+ /* *pcbResult */ uintptr(unsafe.Pointer(&size)),
+ /* dwFlagss */ uintptr(flags))
+ if r != 0 {
+ return nil, fmt.Errorf("NCryptSignHash: failed to get signature: %#x", r)
+ }
+ if len(sig) != int(size) {
+ return nil, fmt.Errorf("invalid length sig = %d, size = %d", sig, size)
+ }
+
+ switch pub := pub.(type) {
+ case *ecdsa.PublicKey:
+ var b cryptobyte.Builder
+ b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
+ b.AddASN1BigInt(new(big.Int).SetBytes(sig[:len(sig)/2]))
+ b.AddASN1BigInt(new(big.Int).SetBytes(sig[len(sig)/2:]))
+ })
+ return b.Bytes()
+ case *rsa.PublicKey:
+ return sig, nil
+ default:
+ return nil, fmt.Errorf("unsupported public key type %T", pub)
+ }
+}
+
+// SignHash is a wrapper for the NCryptSignHash function that supports only a
+// subset of well-supported cryptographic primitives.
+//
+// Signature algorithms: ECDSA, RSA.
+// Hash functions: SHA-256.
+// RSA schemes: RSASSA-PKCS1 and RSASSA-PSS.
+//
+// https://docs.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptsignhash
+func SignHash(priv windows.Handle, pub crypto.PublicKey, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
+ var paddingInfo unsafe.Pointer
+ flags := nCryptSilentFlag
+ switch pub := pub.(type) {
+ case *ecdsa.PublicKey:
+ case *rsa.PublicKey:
+ var err error
+ paddingInfo, err = rsaPadding(opts, &flags)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("unsupported public key type %T", pub)
+ }
+
+ return signHashInternal(priv, pub, digest, flags, paddingInfo)
+}
diff --git a/internal/signer/windows/signer.go b/internal/signer/windows/signer.go
new file mode 100644
index 0000000..9ef64ab
--- /dev/null
+++ b/internal/signer/windows/signer.go
@@ -0,0 +1,132 @@
+// 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 Windows OS using ncrypt 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"
+ "signer/ncrypt"
+ "signer/util"
+ "time"
+)
+
+// 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{})
+}
+
+// SignArgs contains arguments to a crypto Signer.Sign method.
+type SignArgs struct {
+ Digest []byte // The content to sign.
+ Opts crypto.SignerOpts // Options for signing, such as Hash identifier.
+}
+
+// A EnterpriseCertSigner exports RPC methods for signing.
+type EnterpriseCertSigner struct {
+ key *ncrypt.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 specified by args and writes the output to resp.
+func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) {
+ *resp, err = k.key.Sign(nil, args.Digest, 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 = ncrypt.Cred(config.CertConfigs.WindowsStore.Issuer, config.CertConfigs.WindowsStore.Store, config.CertConfigs.WindowsStore.Provider)
+ if err != nil {
+ log.Fatalf("Failed to initialize enterprise cert signer using ncrypt: %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})
+}
diff --git a/internal/signer/windows/util/test_data/certificate_config.json b/internal/signer/windows/util/test_data/certificate_config.json
new file mode 100644
index 0000000..567f719
--- /dev/null
+++ b/internal/signer/windows/util/test_data/certificate_config.json
@@ -0,0 +1,9 @@
+{
+ "cert_configs": {
+ "windows_store": {
+ "issuer": "enterprise_v1_corp_client",
+ "store": "MY",
+ "provider": "current_user"
+ }
+ }
+}
diff --git a/internal/signer/windows/util/util.go b/internal/signer/windows/util/util.go
new file mode 100644
index 0000000..a2bb1bd
--- /dev/null
+++ b/internal/signer/windows/util/util.go
@@ -0,0 +1,57 @@
+// 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 ECP Configs.
+type CertConfigs struct {
+ WindowsStore WindowsStore `json:"windows_store"`
+}
+
+// WindowsStore contains parameters describing the certificate to use.
+type WindowsStore struct {
+ Issuer string `json:"issuer"`
+ Store string `json:"store"`
+ Provider string `json:"provider"`
+}
+
+// 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
+
+}
diff --git a/internal/signer/windows/util/util_test.go b/internal/signer/windows/util/util_test.go
new file mode 100644
index 0000000..89ad6e6
--- /dev/null
+++ b/internal/signer/windows/util/util_test.go
@@ -0,0 +1,37 @@
+// 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")
+ if err != nil {
+ t.Errorf("LoadConfig error: %q", err)
+ }
+ 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)
+ }
+}
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..67b4587
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,6 @@
+{
+ "extends": [
+ "config:base",
+ ":disableDependencyDashboard"
+ ]
+}