├── .github ├── CONTRIBUTING.md └── workflows │ └── test.yml ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── pkg └── gcpkms ├── decrypter.go ├── decrypter_test.go ├── gcpkms.go ├── gcpkms_doc_test.go ├── gcpkms_test.go ├── signer.go └── signer_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | 7 | ## Code reviews 8 | 9 | All submissions, including submissions by project members, require review. We 10 | use GitHub pull requests for this purpose. Consult 11 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 12 | information on using pull requests. 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright The Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: 'Test' 16 | 17 | on: 18 | push: 19 | branches: 20 | - 'main' 21 | tags: 22 | - '*' 23 | pull_request: 24 | branches: 25 | - 'main' 26 | workflow_dispatch: 27 | 28 | jobs: 29 | test: 30 | runs-on: 'ubuntu-latest' 31 | 32 | steps: 33 | - uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4 34 | 35 | - uses: 'actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34' # ratchet:actions/setup-go@v5 36 | with: 37 | go-version-file: 'go.mod' 38 | 39 | - uses: 'google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f' # ratchet:google-github-actions/auth@v2 40 | with: 41 | credentials_json: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} 42 | 43 | - name: 'Test' 44 | env: 45 | TEST_DECRYPTER_KEY: 'projects/sethvargo-gcpkms-test/locations/global/keyRings/test-keyring/cryptoKeys/decrypter/cryptoKeyVersions/1' 46 | TEST_SIGNER_KEY: 'projects/sethvargo-gcpkms-test/locations/global/keyRings/test-keyring/cryptoKeys/signer/cryptoKeyVersions/1' 47 | run: |- 48 | go test \ 49 | -count=1 \ 50 | -race \ 51 | -timeout=10m \ 52 | -shuffle=on \ 53 | ./... 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright The Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | .env 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of project authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | 7 | Google LLC 8 | Seth Vargo 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud KMS - Golang Crypto Interface 2 | 3 | [![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godoc] 4 | 5 | This package wraps the [Google Cloud KMS][cloud-kms] Go library to implement 6 | Go's [crypto.Decrypter][crypto.decrypter] and [crypto.Signer][crypto.signer] 7 | interfaces. It only works with Google Cloud KMS asymmetric keys. 8 | 9 | ## Usage 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | kms "cloud.google.com/go/kms/apiv1" 16 | "github.com/sethvargo/go-gcpkms/pkg/gcpkms" 17 | ) 18 | 19 | func main() { 20 | ctx := context.Background() 21 | kmsClient, err := kms.NewKeyManagementClient(ctx) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | keyID := "projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" 27 | signer, err := gcpkms.NewSigner(ctx, kmsClient, keyID) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | sig, err := signer.Sign(nil, digest, nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | } 37 | ``` 38 | 39 | For more examples, please see the [package godoc][godoc]. 40 | 41 | [cloud-kms]: https://cloud.google.com/kms 42 | [crypto.decrypter]: https://golang.org/pkg/crypto/#Decrypter 43 | [crypto.signer]: https://golang.org/pkg/crypto/#Signer 44 | [godoc]: https://pkg.go.dev/mod/github.com/sethvargo/go-gcpkms 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module github.com/sethvargo/go-gcpkms 16 | 17 | go 1.22.0 18 | 19 | toolchain go1.23.5 20 | 21 | require cloud.google.com/go/kms v1.20.5 22 | 23 | require ( 24 | cloud.google.com/go v0.118.0 // indirect 25 | cloud.google.com/go/auth v0.14.0 // indirect 26 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 27 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 28 | cloud.google.com/go/iam v1.3.1 // indirect 29 | cloud.google.com/go/longrunning v0.6.4 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/google/s2a-go v0.1.9 // indirect 34 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 35 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 36 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 37 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect 38 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 39 | go.opentelemetry.io/otel v1.34.0 // indirect 40 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 41 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 42 | golang.org/x/crypto v0.32.0 // indirect 43 | golang.org/x/net v0.34.0 // indirect 44 | golang.org/x/oauth2 v0.25.0 // indirect 45 | golang.org/x/sync v0.10.0 // indirect 46 | golang.org/x/sys v0.29.0 // indirect 47 | golang.org/x/text v0.21.0 // indirect 48 | golang.org/x/time v0.9.0 // indirect 49 | google.golang.org/api v0.217.0 // indirect 50 | google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f // indirect 51 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect 52 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 53 | google.golang.org/grpc v1.69.4 // indirect 54 | google.golang.org/protobuf v1.36.3 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ= 2 | cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM= 3 | cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM= 4 | cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 7 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 8 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 9 | cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E= 10 | cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34= 11 | cloud.google.com/go/kms v1.20.5 h1:aQQ8esAIVZ1atdJRxihhdxGQ64/zEbJoJnCz/ydSmKg= 12 | cloud.google.com/go/kms v1.20.5/go.mod h1:C5A8M1sv2YWYy1AE6iSrnddSG9lRGdJq5XEdBy28Lmw= 13 | cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= 14 | cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 18 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 19 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 20 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 21 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 22 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 23 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 24 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 26 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 27 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 28 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 29 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 31 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 35 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 36 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 37 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 41 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 42 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 43 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 44 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 45 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 46 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= 47 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 49 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 50 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 51 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 52 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 53 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 54 | go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= 55 | go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= 56 | go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= 57 | go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= 58 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 59 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 60 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 61 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 62 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 63 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 64 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 65 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 66 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 67 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 68 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 69 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 70 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 71 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 72 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 73 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 74 | google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU= 75 | google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= 76 | google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f h1:387Y+JbxF52bmesc8kq1NyYIp33dnxCw6eiA7JMsTmw= 77 | google.golang.org/genproto v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:0joYwWwLQh18AOj8zMYeZLjzuqcYTU3/nC5JdCvC3JI= 78 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= 79 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= 80 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 81 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 82 | google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= 83 | google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 84 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 85 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 86 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 87 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | -------------------------------------------------------------------------------- /pkg/gcpkms/decrypter.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms 16 | 17 | import ( 18 | "context" 19 | "crypto" 20 | "fmt" 21 | "io" 22 | "sync" 23 | 24 | kms "cloud.google.com/go/kms/apiv1" 25 | "cloud.google.com/go/kms/apiv1/kmspb" 26 | ) 27 | 28 | // Decrypter implements crypto.Decrypter. 29 | var _ crypto.Decrypter = (*Decrypter)(nil) 30 | 31 | // Decrypter implements crypto.Decrypter for Google Cloud KMS keys. 32 | type Decrypter struct { 33 | ctx context.Context 34 | ctxLock sync.RWMutex 35 | 36 | client *kms.KeyManagementClient 37 | keyID string 38 | keyAlgorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm 39 | publicKey crypto.PublicKey 40 | } 41 | 42 | // NewDecrypter creates a new decrypter. The keyID must be in the 43 | // format projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/v. 44 | func NewDecrypter(ctx context.Context, client *kms.KeyManagementClient, keyID string) (*Decrypter, error) { 45 | if client == nil { 46 | return nil, fmt.Errorf("kms client cannot be nil") 47 | } 48 | 49 | // Get the public key 50 | pk, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ 51 | Name: keyID, 52 | }) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to fetch public key: %w", err) 55 | } 56 | 57 | // Verify it's a key used for decryption 58 | switch pk.Algorithm { 59 | case kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_2048_SHA256, 60 | kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_3072_SHA256, 61 | kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA256, 62 | kmspb.CryptoKeyVersion_RSA_DECRYPT_OAEP_4096_SHA512: 63 | default: 64 | return nil, fmt.Errorf("unknown decryption algorithm %s", pk.Algorithm.String()) 65 | } 66 | 67 | // Parse the public key 68 | publicKey, err := parsePublicKey([]byte(pk.Pem)) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to parse public key: %w", err) 71 | } 72 | 73 | return &Decrypter{ 74 | client: client, 75 | keyID: keyID, 76 | keyAlgorithm: pk.Algorithm, 77 | publicKey: publicKey, 78 | }, nil 79 | } 80 | 81 | // WithContext adds the given context to the decrypter. Normally this would be 82 | // passed as the first argument to Decrypt, but the current interface does not 83 | // accept a context. 84 | func (d *Decrypter) WithContext(ctx context.Context) *Decrypter { 85 | d.ctxLock.Lock() 86 | defer d.ctxLock.Unlock() 87 | 88 | d.ctx = ctx 89 | return d 90 | } 91 | 92 | // Public returns the public key for the decrypter. 93 | func (d *Decrypter) Public() crypto.PublicKey { 94 | return d.publicKey 95 | } 96 | 97 | // Decrypt decrypts the given message. 98 | func (d *Decrypter) Decrypt(_ io.Reader, msg []byte, _ crypto.DecrypterOpts) ([]byte, error) { 99 | ctx := d.context() 100 | resp, err := d.client.AsymmetricDecrypt(ctx, &kmspb.AsymmetricDecryptRequest{ 101 | Name: d.keyID, 102 | Ciphertext: msg, 103 | }) 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to decrypt ciphertext: %w", err) 106 | } 107 | return resp.Plaintext, nil 108 | } 109 | 110 | // context returns the context for this decrypter. 111 | func (d *Decrypter) context() context.Context { 112 | d.ctxLock.RLock() 113 | defer d.ctxLock.RUnlock() 114 | 115 | ctx := d.ctx 116 | if ctx == nil { 117 | ctx = context.Background() 118 | } 119 | 120 | return ctx 121 | } 122 | -------------------------------------------------------------------------------- /pkg/gcpkms/decrypter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms 16 | 17 | import ( 18 | "context" 19 | "crypto/rand" 20 | "crypto/rsa" 21 | "crypto/sha512" 22 | "strings" 23 | "testing" 24 | 25 | kms "cloud.google.com/go/kms/apiv1" 26 | ) 27 | 28 | func TestNewDecrypter(t *testing.T) { 29 | t.Parallel() 30 | 31 | client, ctx := testClient(t) 32 | 33 | cases := []struct { 34 | name string 35 | client *kms.KeyManagementClient 36 | ckv string 37 | err string 38 | }{ 39 | { 40 | name: "nil client", 41 | client: nil, 42 | ckv: "", 43 | err: "cannot be nil", 44 | }, 45 | { 46 | name: "bad key", 47 | client: client, 48 | ckv: "nope nope nope", 49 | err: "failed to fetch public key", 50 | }, 51 | { 52 | name: "ok", 53 | client: client, 54 | ckv: testDecrypterKey, 55 | }, 56 | } 57 | 58 | for _, tc := range cases { 59 | tc := tc 60 | 61 | t.Run(tc.name, func(t *testing.T) { 62 | t.Parallel() 63 | 64 | if _, err := NewDecrypter(ctx, tc.client, tc.ckv); err != nil { 65 | if tc.err != "" { 66 | if !strings.Contains(err.Error(), tc.err) { 67 | t.Errorf("expected %q to contain %q", err.Error(), tc.err) 68 | } 69 | } else { 70 | t.Fatal(err) 71 | } 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestDecrypter_WithContext(t *testing.T) { 78 | t.Parallel() 79 | 80 | d := new(Decrypter) 81 | ctx := context.Background() 82 | d = d.WithContext(ctx) 83 | 84 | if ctx != d.ctx { 85 | t.Fatalf("expected %#v to be %#v", ctx, d.ctx) 86 | } 87 | } 88 | 89 | func TestDecrypter_Public(t *testing.T) { 90 | t.Parallel() 91 | 92 | client, ctx := testClient(t) 93 | decrypter, err := NewDecrypter(ctx, client, testDecrypterKey) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | if p := decrypter.Public(); p == nil { 99 | t.Errorf("expected public key") 100 | } 101 | } 102 | 103 | func TestDecrypter_Decrypt(t *testing.T) { 104 | t.Parallel() 105 | 106 | client, ctx := testClient(t) 107 | decrypter, err := NewDecrypter(ctx, client, testDecrypterKey) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | // Get the public key to encrypt the data 113 | pub, ok := decrypter.Public().(*rsa.PublicKey) 114 | if !ok { 115 | t.Fatalf("expected %T to be *rsa.PublicKey", decrypter.Public()) 116 | } 117 | 118 | msg := []byte("my message to encrypt") 119 | hsh := sha512.New() 120 | ciphertext, err := rsa.EncryptOAEP(hsh, rand.Reader, pub, msg, nil) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | plaintext, err := decrypter.Decrypt(nil, ciphertext[:], nil) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | if p, m := string(plaintext), string(msg); p != m { 131 | t.Errorf("expected %q to be %q", p, m) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/gcpkms/gcpkms.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package gcpkms wraps the Google Cloud KMS Go library to implement Go's 16 | // crypto.Decrypter crypto.Signer interfaces. 17 | package gcpkms 18 | 19 | import ( 20 | "crypto" 21 | "crypto/ecdsa" 22 | "crypto/ed25519" 23 | "crypto/rsa" 24 | "crypto/x509" 25 | "encoding/pem" 26 | "fmt" 27 | ) 28 | 29 | // parsePublicKey extracts the pem-encoded contents and parses it as a public 30 | // key. 31 | func parsePublicKey(p []byte) (crypto.PublicKey, error) { 32 | block, _ := pem.Decode(p) 33 | if block == nil { 34 | return nil, fmt.Errorf("pem is invalid") 35 | } 36 | 37 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to parse public key: %w", err) 40 | } 41 | 42 | switch t := pub.(type) { 43 | case *rsa.PublicKey: 44 | return t, nil 45 | case *ecdsa.PublicKey: 46 | return t, nil 47 | case ed25519.PublicKey: 48 | return t, nil 49 | default: 50 | return nil, fmt.Errorf("unknown key type %T", t) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/gcpkms/gcpkms_doc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms_test 16 | 17 | import ( 18 | "context" 19 | "crypto/sha512" 20 | "fmt" 21 | "log" 22 | 23 | kms "cloud.google.com/go/kms/apiv1" 24 | "github.com/sethvargo/go-gcpkms/pkg/gcpkms" 25 | ) 26 | 27 | var ( 28 | ctx = context.Background() 29 | kmsClient, _ = kms.NewKeyManagementClient(ctx) 30 | ) 31 | 32 | func ExampleSigner_Sign() { 33 | // Key is the full resource name 34 | keyID := "projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" 35 | 36 | // Create the signer 37 | signer, err := gcpkms.NewSigner(ctx, kmsClient, keyID) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | // Message to sign 43 | msg := []byte("my message to sign") 44 | 45 | // Hash the message - this hash must correspond to the KMS key type 46 | dig := sha512.Sum512(msg) 47 | 48 | // Sign the hash 49 | sig, err := signer.Sign(nil, dig[:], nil) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | fmt.Println(string(sig)) 55 | } 56 | 57 | func ExampleDecrypter_Decrypt() { 58 | // Key is the full resource name 59 | keyID := "projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1" 60 | 61 | // Create the decrypter 62 | decrypter, err := gcpkms.NewDecrypter(ctx, kmsClient, keyID) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | // Ciphertext to decrypt - this ciphertext would have been encrypted with the 68 | // public key 69 | ciphertext := []byte("...") 70 | 71 | // Decrypt the ciphertext 72 | plaintext, err := decrypter.Decrypt(nil, ciphertext, nil) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | fmt.Println(string(plaintext)) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/gcpkms/gcpkms_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "testing" 22 | 23 | kms "cloud.google.com/go/kms/apiv1" 24 | ) 25 | 26 | var ( 27 | testDecrypterKey string 28 | testSignerKey string 29 | ) 30 | 31 | func testClient(tb testing.TB) (*kms.KeyManagementClient, context.Context) { 32 | tb.Helper() 33 | 34 | ctx := context.Background() 35 | client, err := kms.NewKeyManagementClient(ctx) 36 | if err != nil { 37 | tb.Fatal(err) 38 | } 39 | return client, ctx 40 | } 41 | 42 | func TestMain(m *testing.M) { 43 | setFromEnv(&testDecrypterKey, "TEST_DECRYPTER_KEY") 44 | setFromEnv(&testSignerKey, "TEST_SIGNER_KEY") 45 | 46 | os.Exit(m.Run()) 47 | } 48 | 49 | func setFromEnv(s *string, k string) { 50 | v := os.Getenv(k) 51 | if v == "" { 52 | fmt.Fprintf(os.Stderr, "missing %s\n", k) 53 | os.Exit(1) 54 | } 55 | *s = v 56 | } 57 | -------------------------------------------------------------------------------- /pkg/gcpkms/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms 16 | 17 | import ( 18 | "context" 19 | "crypto" 20 | "fmt" 21 | "io" 22 | "sync" 23 | 24 | kms "cloud.google.com/go/kms/apiv1" 25 | "cloud.google.com/go/kms/apiv1/kmspb" 26 | ) 27 | 28 | // Signer implements crypto.Signer. 29 | var _ crypto.Signer = (*Signer)(nil) 30 | 31 | // Signer implements crypto.Signer for Google Cloud KMS keys. 32 | type Signer struct { 33 | ctx context.Context 34 | ctxLock sync.RWMutex 35 | 36 | client *kms.KeyManagementClient 37 | keyID string 38 | publicKey crypto.PublicKey 39 | digestAlg crypto.Hash 40 | } 41 | 42 | // NewSigner creates a new signer. The keyID must be in the format 43 | // projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/v. 44 | func NewSigner(ctx context.Context, client *kms.KeyManagementClient, keyID string) (*Signer, error) { 45 | if client == nil { 46 | return nil, fmt.Errorf("kms client cannot be nil") 47 | } 48 | 49 | // Get the public key 50 | pk, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{ 51 | Name: keyID, 52 | }) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to fetch public key: %w", err) 55 | } 56 | 57 | // Verify it's a key used for signing 58 | var digestAlg crypto.Hash 59 | switch pk.Algorithm { 60 | case kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256, 61 | kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, 62 | kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256, 63 | kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, 64 | kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, 65 | kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, 66 | kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: 67 | digestAlg = crypto.SHA256 68 | case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: 69 | digestAlg = crypto.SHA384 70 | case kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512, 71 | kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512: 72 | digestAlg = crypto.SHA512 73 | default: 74 | return nil, fmt.Errorf("unknown signing algorithm %s", pk.Algorithm.String()) 75 | } 76 | 77 | // Parse the public key 78 | publicKey, err := parsePublicKey([]byte(pk.Pem)) 79 | if err != nil { 80 | return nil, fmt.Errorf("failed to parse public key: %w", err) 81 | } 82 | 83 | return &Signer{ 84 | client: client, 85 | keyID: keyID, 86 | publicKey: publicKey, 87 | digestAlg: digestAlg, 88 | }, nil 89 | } 90 | 91 | // Public returns the public key for the signer. 92 | func (s *Signer) Public() crypto.PublicKey { 93 | return s.publicKey 94 | } 95 | 96 | // DigestAlgorithm returns the hash algorithm used for computing the digest. 97 | func (s *Signer) DigestAlgorithm() crypto.Hash { 98 | return s.digestAlg 99 | } 100 | 101 | // WithContext adds the given context to the signer. Normally this would be 102 | // passed as the first argument to Sign, but the current interface does not 103 | // accept a context. 104 | func (s *Signer) WithContext(ctx context.Context) *Signer { 105 | s.ctxLock.Lock() 106 | defer s.ctxLock.Unlock() 107 | 108 | s.ctx = ctx 109 | return s 110 | } 111 | 112 | // Sign signs the given digest. Both the io.Reader and crypto.SignerOpts are 113 | // unused. 114 | func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { 115 | ctx := s.context() 116 | 117 | // Make sure the opts and digest length are correct 118 | if opts != nil && opts.HashFunc() != s.digestAlg { 119 | return nil, fmt.Errorf("digest algorithm is %d, want %d", opts.HashFunc(), s.digestAlg) 120 | } 121 | if len(digest) != s.digestAlg.Size() { 122 | return nil, fmt.Errorf("digest length is %d, want %d", len(digest), s.digestAlg.Size()) 123 | } 124 | 125 | // Set the correct digest based on the key's digest algorithm 126 | var dig *kmspb.Digest 127 | switch s.digestAlg { 128 | case crypto.SHA256: 129 | dig = &kmspb.Digest{Digest: &kmspb.Digest_Sha256{Sha256: digest}} 130 | case crypto.SHA384: 131 | dig = &kmspb.Digest{Digest: &kmspb.Digest_Sha384{Sha384: digest}} 132 | case crypto.SHA512: 133 | dig = &kmspb.Digest{Digest: &kmspb.Digest_Sha512{Sha512: digest}} 134 | } 135 | 136 | // Sign the digest 137 | resp, err := s.client.AsymmetricSign(ctx, &kmspb.AsymmetricSignRequest{ 138 | Name: s.keyID, 139 | Digest: dig, 140 | }) 141 | if err != nil { 142 | return nil, fmt.Errorf("failed to sign: %w", err) 143 | } 144 | 145 | return resp.Signature, nil 146 | } 147 | 148 | // context returns the context for this signer or 149 | func (s *Signer) context() context.Context { 150 | s.ctxLock.RLock() 151 | defer s.ctxLock.RUnlock() 152 | 153 | ctx := s.ctx 154 | if ctx == nil { 155 | ctx = context.Background() 156 | } 157 | 158 | return ctx 159 | } 160 | -------------------------------------------------------------------------------- /pkg/gcpkms/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gcpkms 16 | 17 | import ( 18 | "context" 19 | "crypto/sha512" 20 | "strings" 21 | "testing" 22 | 23 | kms "cloud.google.com/go/kms/apiv1" 24 | ) 25 | 26 | func TestNewSigner(t *testing.T) { 27 | t.Parallel() 28 | 29 | client, ctx := testClient(t) 30 | 31 | cases := []struct { 32 | name string 33 | client *kms.KeyManagementClient 34 | ckv string 35 | err string 36 | }{ 37 | { 38 | name: "nil client", 39 | client: nil, 40 | ckv: "", 41 | err: "cannot be nil", 42 | }, 43 | { 44 | name: "bad key", 45 | client: client, 46 | ckv: "nope nope nope", 47 | err: "failed to fetch public key", 48 | }, 49 | { 50 | name: "ok", 51 | client: client, 52 | ckv: testSignerKey, 53 | }, 54 | } 55 | 56 | for _, tc := range cases { 57 | tc := tc 58 | 59 | t.Run(tc.name, func(t *testing.T) { 60 | t.Parallel() 61 | 62 | if _, err := NewSigner(ctx, tc.client, tc.ckv); err != nil { 63 | if tc.err != "" { 64 | if !strings.Contains(err.Error(), tc.err) { 65 | t.Errorf("expected %q to contain %q", err.Error(), tc.err) 66 | } 67 | } else { 68 | t.Fatal(err) 69 | } 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestSigner_WithContext(t *testing.T) { 76 | t.Parallel() 77 | 78 | s := new(Signer) 79 | ctx := context.Background() 80 | s = s.WithContext(ctx) 81 | 82 | if ctx != s.ctx { 83 | t.Fatalf("expected %#v to be %#v", ctx, s.ctx) 84 | } 85 | } 86 | 87 | func TestSigner_Public(t *testing.T) { 88 | t.Parallel() 89 | 90 | client, ctx := testClient(t) 91 | signer, err := NewSigner(ctx, client, testSignerKey) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if p := signer.Public(); p == nil { 97 | t.Errorf("expected public key") 98 | } 99 | } 100 | 101 | func TestSigner_Sign(t *testing.T) { 102 | t.Parallel() 103 | 104 | client, ctx := testClient(t) 105 | signer, err := NewSigner(ctx, client, testSignerKey) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | msg := "my message to sign" 111 | dig := sha512.Sum512([]byte(msg)) 112 | 113 | sig, err := signer.Sign(nil, dig[:], nil) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | if len(sig) < 10 { 119 | t.Errorf("invalid signature: %s", sig) 120 | } 121 | } 122 | --------------------------------------------------------------------------------