├── LICENSE ├── README.md ├── docs ├── CONTRIBUTING.md └── SECURITY.md ├── go.mod ├── go.sum ├── integration └── gcpkms │ ├── gcp_kms_aead.go │ ├── gcp_kms_aead_test.go │ ├── gcp_kms_client.go │ ├── gcp_kms_client_test.go │ ├── gcp_kms_grpc_aead.go │ ├── gcp_kms_grpc_aead_test.go │ └── gcp_kms_integration_test.go ├── kokoro ├── create_github_release_branch.sh ├── create_github_release_tag.sh ├── gcp_ubuntu │ └── gomod │ │ └── run_tests.sh ├── macos_external │ └── gomod │ │ └── run_tests.sh └── testutils │ ├── BUILD.bazel │ ├── check_go_generated_files_up_to_date.sh │ ├── copy_credentials.sh │ ├── docker_execute.sh │ ├── github_release_util.sh │ ├── github_release_util_test.sh │ ├── go_test_container_images.sh │ ├── install_go.sh │ └── test_utils.sh └── testdata └── gcp ├── BUILD.bazel ├── README.md ├── credential.json ├── credential_bad.json ├── key_name.txt └── key_name_bad.txt /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 | # Tink Go Google Cloud KMS extension 2 | 3 | 4 | 5 | [tink_go_gcpkms_gomod_badge_gcp_ubuntu]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-go-gcpkms-gomod-gcp-ubuntu.svg 6 | 7 | 8 | 9 | [tink_go_gcpkms_gomod_badge_macos]: https://storage.googleapis.com/tink-kokoro-build-badges/tink-go-gcpkms-gomod-macos-external.svg 10 | 11 | **Test** | **GCP Ubuntu** | **MacOS** 12 | -------- | -------------------------------------------------------------- | --------- 13 | Gomod | [![Gomod_GcpUbuntu][tink_go_gcpkms_gomod_badge_gcp_ubuntu]](#) | [![Gomod_MacOs][tink_go_gcpkms_gomod_badge_macos]](#) 14 | 15 | This is an extension to the [Tink Go](https://github.com/tink-crypto/tink-go) 16 | library that provides support for Google Cloud KMS. 17 | 18 | The latest version is 2.2.0. 19 | 20 | The official documentation is available at https://developers.google.com/tink. 21 | 22 | ## Contact and mailing list 23 | 24 | If you want to contribute, please read [CONTRIBUTING](docs/CONTRIBUTING.md) and 25 | send us pull requests. You can also report bugs or file feature requests. 26 | 27 | If you'd like to talk to the developers or get notified about major product 28 | updates, you may want to subscribe to our 29 | [mailing list](https://groups.google.com/forum/#!forum/tink-users). 30 | 31 | ## Maintainers 32 | 33 | Tink is maintained by (A-Z): 34 | 35 | - Moreno Ambrosin 36 | - Taymon Beal 37 | - William Conner 38 | - Thomas Holenstein 39 | - Stefan Kölbl 40 | - Charles Lee 41 | - Cindy Lin 42 | - Fernando Lobato Meeser 43 | - Ioana Nedelcu 44 | - Sophie Schmieg 45 | - Elizaveta Tretiakova 46 | - Jürg Wullschleger 47 | 48 | Alumni: 49 | 50 | - Haris Andrianakis 51 | - Daniel Bleichenbacher 52 | - Tanuj Dhir 53 | - Thai Duong 54 | - Atul Luykx 55 | - Rafael Misoczki 56 | - Quan Nguyen 57 | - Bartosz Przydatek 58 | - Enzo Puig 59 | - Laurent Simon 60 | - Veronika Slívová 61 | - Paula Vidas 62 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Please see the 4 | [developer documentation](https://developers.google.com/tink/contributing) on 5 | how to contribute to Tink. 6 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | To report a security issue, please use http://g.co/vulnz. We use 2 | http://g.co/vulnz for our intake and coordination, and disclose vulnerabilities 3 | using GitHub Security Advisory. The Google Security Team will 4 | respond within 5 working days of your report on g.co/vulnz. 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tink-crypto/tink-go-gcpkms/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | cloud.google.com/go/kms v1.20.5 7 | github.com/tink-crypto/tink-go/v2 v2.3.0 8 | google.golang.org/api v0.223.0 9 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 10 | google.golang.org/grpc v1.70.0 11 | google.golang.org/protobuf v1.36.5 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go v0.116.0 // indirect 16 | cloud.google.com/go/auth v0.15.0 // indirect 17 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 18 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 19 | cloud.google.com/go/iam v1.2.2 // indirect 20 | cloud.google.com/go/longrunning v0.6.2 // indirect 21 | github.com/felixge/httpsnoop v1.0.4 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/google/s2a-go v0.1.9 // indirect 25 | github.com/google/uuid v1.6.0 // indirect 26 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 27 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 28 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 29 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect 30 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 31 | go.opentelemetry.io/otel v1.34.0 // indirect 32 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 33 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 34 | golang.org/x/crypto v0.36.0 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/oauth2 v0.26.0 // indirect 37 | golang.org/x/sync v0.12.0 // indirect 38 | golang.org/x/sys v0.31.0 // indirect 39 | golang.org/x/text v0.23.0 // indirect 40 | golang.org/x/time v0.10.0 // indirect 41 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 42 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= 2 | cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= 3 | cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= 4 | cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= 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.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= 10 | cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= 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.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= 14 | cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= 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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 | github.com/tink-crypto/tink-go/v2 v2.3.0 h1:4/TA0lw0lA/iVKBL9f8R5eP7397bfc4antAMXF5JRhs= 43 | github.com/tink-crypto/tink-go/v2 v2.3.0/go.mod h1:kfPOtXIadHlekBTeBtJrHWqoGL+Fm3JQg0wtltPuxLU= 44 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 45 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 46 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 47 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 48 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= 49 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= 50 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 51 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 52 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 53 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 54 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 55 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 56 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 57 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 58 | go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= 59 | go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= 60 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 61 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 62 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 63 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 64 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 65 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 66 | golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= 67 | golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 68 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 69 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 70 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 71 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 72 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 73 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 74 | golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= 75 | golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 76 | google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc= 77 | google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= 78 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= 79 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= 80 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 81 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 82 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= 83 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 84 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 85 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 86 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 87 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_aead.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 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 | "encoding/base64" 19 | "fmt" 20 | "hash/crc32" 21 | 22 | "google.golang.org/api/cloudkms/v1" 23 | 24 | "github.com/tink-crypto/tink-go/v2/tink" 25 | ) 26 | 27 | // gcpAEAD represents a GCP KMS service to a particular URI. 28 | type gcpAEAD struct { 29 | keyName string 30 | kms cloudkms.Service 31 | } 32 | 33 | var _ tink.AEAD = (*gcpAEAD)(nil) 34 | 35 | // newGCPAEAD returns a new GCP KMS service. 36 | func newGCPAEAD(keyName string, kms *cloudkms.Service) tink.AEAD { 37 | return &gcpAEAD{ 38 | keyName: keyName, 39 | kms: *kms, 40 | } 41 | } 42 | 43 | // Encrypt calls GCP KMS to encrypt the plaintext with associatedData and returns the resulting ciphertext. 44 | // It returns an error if the call to KMS fails or if the response returned by KMS does not pass integrity verification 45 | // (http://cloud.google.com/kms/docs/data-integrity-guidelines#calculating_and_verifying_checksums). 46 | func (a *gcpAEAD) Encrypt(plaintext, associatedData []byte) ([]byte, error) { 47 | 48 | req := &cloudkms.EncryptRequest{ 49 | Plaintext: base64.URLEncoding.EncodeToString(plaintext), 50 | PlaintextCrc32c: computeChecksum(plaintext), 51 | AdditionalAuthenticatedData: base64.URLEncoding.EncodeToString(associatedData), 52 | AdditionalAuthenticatedDataCrc32c: computeChecksum(associatedData), 53 | // Send the integrity verification fields even if their value is 0. 54 | ForceSendFields: []string{"PlaintextCrc32c", "AdditionalAuthenticatedDataCrc32c"}, 55 | } 56 | 57 | resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Encrypt(a.keyName, req).Do() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if !resp.VerifiedPlaintextCrc32c { 63 | return nil, fmt.Errorf("KMS request for %q is missing the checksum field plaintext_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyName) 64 | } 65 | if !resp.VerifiedAdditionalAuthenticatedDataCrc32c { 66 | return nil, fmt.Errorf("KMS request for %q is missing the checksum field additional_authenticated_data_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyName) 67 | } 68 | ciphertext, err := base64.StdEncoding.DecodeString(resp.Ciphertext) 69 | if err != nil { 70 | return nil, err 71 | } 72 | if resp.CiphertextCrc32c != computeChecksum(ciphertext) { 73 | return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field ciphertext_crc32c did not match the data in field ciphertext. Please retry in case this is a transient error", a.keyName) 74 | } 75 | 76 | return ciphertext, nil 77 | } 78 | 79 | // Decrypt calls GCP KMS to decrypt the ciphertext with with associatedData and returns the resulting plaintext. 80 | // It returns an error if the call to KMS fails or if the response returned by KMS does not pass integrity verification 81 | // (http://cloud.google.com/kms/docs/data-integrity-guidelines#calculating_and_verifying_checksums). 82 | func (a *gcpAEAD) Decrypt(ciphertext, associatedData []byte) ([]byte, error) { 83 | 84 | req := &cloudkms.DecryptRequest{ 85 | Ciphertext: base64.URLEncoding.EncodeToString(ciphertext), 86 | CiphertextCrc32c: computeChecksum(ciphertext), 87 | AdditionalAuthenticatedData: base64.URLEncoding.EncodeToString(associatedData), 88 | AdditionalAuthenticatedDataCrc32c: computeChecksum(associatedData), 89 | // Send the integrity verification fields even if their value is 0. 90 | ForceSendFields: []string{"CiphertextCrc32c", "AdditionalAuthenticatedDataCrc32c"}, 91 | } 92 | 93 | resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Decrypt(a.keyName, req).Do() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | plaintext, err := base64.StdEncoding.DecodeString(resp.Plaintext) 99 | if err != nil { 100 | return nil, err 101 | } 102 | if resp.PlaintextCrc32c != computeChecksum(plaintext) { 103 | return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field plaintext_crc32c did not match the data in field plaintext. Please retry in case this is a transient error", a.keyName) 104 | } 105 | return plaintext, nil 106 | } 107 | 108 | // crc32cTable is used to compute checksums. It is defined as a package level variable to avoid 109 | // re-computation on every CRC calculation. 110 | var crc32cTable = crc32.MakeTable(crc32.Castagnoli) 111 | 112 | // computeChecksum returns the checksum that corresponds to the input value as an int64. 113 | func computeChecksum(value []byte) int64 { 114 | return int64(crc32.Checksum(value, crc32cTable)) 115 | } 116 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_aead_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google Inc. 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 | "bytes" 19 | "context" 20 | "encoding/base64" 21 | "encoding/json" 22 | "hash/crc32" 23 | "net/http" 24 | "net/http/httptest" 25 | "testing" 26 | 27 | "google.golang.org/api/cloudkms/v1" 28 | "google.golang.org/api/option" 29 | ) 30 | 31 | func initializeServerWithResponse(ctx context.Context, t *testing.T, response any) (*httptest.Server, *cloudkms.Service) { 32 | t.Helper() 33 | var b []byte 34 | switch r := response.(type) { 35 | case *cloudkms.EncryptResponse, *cloudkms.DecryptResponse: 36 | var err error 37 | b, err = json.Marshal(r) 38 | if err != nil { 39 | t.Fatalf("unable to marshal response: %v", err) 40 | } 41 | default: 42 | t.Fatalf("unsupported response type: %T", r) 43 | } 44 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | w.Write(b) 46 | })) 47 | svc, err := cloudkms.NewService(ctx, option.WithoutAuthentication(), option.WithEndpoint(ts.URL)) 48 | if err != nil { 49 | t.Fatalf("unable to create client: %v", err) 50 | } 51 | return ts, svc 52 | } 53 | 54 | func TestEncrypt_FailsWhenPlaintextUnverifed(t *testing.T) { 55 | additionalData := []byte("additional data") 56 | ciphertext := []byte("ciphertext") 57 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 58 | 59 | testcases := []struct { 60 | name string 61 | encryptResponse *cloudkms.EncryptResponse 62 | }{ 63 | { 64 | name: "verified_plaintext_crc32c is false", 65 | encryptResponse: &cloudkms.EncryptResponse{ 66 | Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), 67 | CiphertextCrc32c: ciphertextCrc32c, 68 | VerifiedPlaintextCrc32c: false, 69 | VerifiedAdditionalAuthenticatedDataCrc32c: true, 70 | }, 71 | }, 72 | { 73 | name: "verified_plaintext_crc32c missing", 74 | encryptResponse: &cloudkms.EncryptResponse{ 75 | Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), 76 | CiphertextCrc32c: ciphertextCrc32c, 77 | VerifiedAdditionalAuthenticatedDataCrc32c: true, 78 | }, 79 | }, 80 | } 81 | 82 | for _, tc := range testcases { 83 | t.Run(tc.name, func(t *testing.T) { 84 | ctx := context.Background() 85 | ts, svc := initializeServerWithResponse(ctx, t, tc.encryptResponse) 86 | defer ts.Close() 87 | 88 | aead := newGCPAEAD("key name", svc) 89 | // Encryption should fail for all plaintexts (empty or non-empty) 90 | _, err := aead.Encrypt([]byte("plaintext"), additionalData) 91 | if err == nil { 92 | t.Errorf("a.Encrypt err = nil, want error") 93 | } 94 | _, err = aead.Encrypt([]byte(""), additionalData) 95 | if err == nil { 96 | t.Errorf("a.Encrypt err = nil, want error") 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestEncrypt_FailsWhenAdditionalAuthenticatedDataUnverifed(t *testing.T) { 103 | plaintext := []byte("plaintext") 104 | ciphertext := []byte("ciphertext") 105 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 106 | 107 | testcases := []struct { 108 | name string 109 | encryptResponse *cloudkms.EncryptResponse 110 | }{ 111 | { 112 | name: "verified_additional_authenticated_data_crc32c is false", 113 | encryptResponse: &cloudkms.EncryptResponse{ 114 | Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), 115 | CiphertextCrc32c: ciphertextCrc32c, 116 | VerifiedPlaintextCrc32c: true, 117 | VerifiedAdditionalAuthenticatedDataCrc32c: false, 118 | }, 119 | }, 120 | { 121 | name: "verified_additional_authenticated_data_crc32c missing", 122 | encryptResponse: &cloudkms.EncryptResponse{ 123 | Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), 124 | CiphertextCrc32c: ciphertextCrc32c, 125 | VerifiedPlaintextCrc32c: true, 126 | }, 127 | }, 128 | } 129 | 130 | for _, tc := range testcases { 131 | t.Run(tc.name, func(t *testing.T) { 132 | ctx := context.Background() 133 | ts, svc := initializeServerWithResponse(ctx, t, tc.encryptResponse) 134 | defer ts.Close() 135 | 136 | aead := newGCPAEAD("key name", svc) 137 | // Encryption should fail for all additional authenticated data (empty or non-empty) 138 | _, err := aead.Encrypt(plaintext, []byte("additional data")) 139 | if err == nil { 140 | t.Errorf("a.Encrypt err = nil, want error") 141 | } 142 | _, err = aead.Encrypt(plaintext, []byte("")) 143 | if err == nil { 144 | t.Errorf("a.Encrypt err = nil, want error") 145 | } 146 | }) 147 | } 148 | } 149 | 150 | func TestEncrypt_FailsWithInvalidCiphertextCrc32c(t *testing.T) { 151 | testcases := []struct { 152 | name string 153 | encryptResponse *cloudkms.EncryptResponse 154 | }{ 155 | { 156 | name: "ciphertext_crc32c does not match ciphertext", 157 | encryptResponse: &cloudkms.EncryptResponse{ 158 | Ciphertext: base64.StdEncoding.EncodeToString([]byte("ciphertext")), 159 | CiphertextCrc32c: int64(1), 160 | VerifiedPlaintextCrc32c: true, 161 | VerifiedAdditionalAuthenticatedDataCrc32c: true, 162 | }, 163 | }, 164 | { 165 | name: "ciphertext_crc32c missing", 166 | encryptResponse: &cloudkms.EncryptResponse{ 167 | Ciphertext: base64.StdEncoding.EncodeToString([]byte("ciphertext")), 168 | VerifiedPlaintextCrc32c: true, 169 | VerifiedAdditionalAuthenticatedDataCrc32c: true, 170 | }, 171 | }, 172 | } 173 | 174 | for _, tc := range testcases { 175 | t.Run(tc.name, func(t *testing.T) { 176 | ctx := context.Background() 177 | ts, svc := initializeServerWithResponse(ctx, t, 178 | tc.encryptResponse) 179 | defer ts.Close() 180 | 181 | aead := newGCPAEAD("key name", svc) 182 | _, err := aead.Encrypt([]byte("plaintext"), []byte("additional data")) 183 | if err == nil { 184 | t.Errorf("a.Encrypt err = nil, want error") 185 | } 186 | }) 187 | } 188 | } 189 | 190 | func TestEncrypt_Success(t *testing.T) { 191 | ciphertext := []byte("ciphertext") 192 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 193 | 194 | ctx := context.Background() 195 | ts, svc := initializeServerWithResponse(ctx, t, 196 | &cloudkms.EncryptResponse{ 197 | Ciphertext: base64.StdEncoding.EncodeToString(ciphertext), 198 | CiphertextCrc32c: ciphertextCrc32c, 199 | VerifiedPlaintextCrc32c: true, 200 | VerifiedAdditionalAuthenticatedDataCrc32c: true, 201 | }) 202 | defer ts.Close() 203 | 204 | aead := newGCPAEAD("key name", svc) 205 | gotCiphertext, err := aead.Encrypt([]byte("plaintext"), []byte("additional data")) 206 | if err != nil { 207 | t.Errorf("a.Encrypt err = %q, want nil", err) 208 | } 209 | if !bytes.Equal(gotCiphertext, ciphertext) { 210 | t.Errorf("Returned ciphertext: %q, want: %q", gotCiphertext, ciphertext) 211 | } 212 | } 213 | 214 | func TestDecrypt_FailsWithInvalidPlaintextCrc32c(t *testing.T) { 215 | testcases := []struct { 216 | name string 217 | decryptResponse *cloudkms.DecryptResponse 218 | }{ 219 | { 220 | name: "plaintext_crc32c does not match plaintext", 221 | decryptResponse: &cloudkms.DecryptResponse{ 222 | Plaintext: base64.StdEncoding.EncodeToString([]byte("plaintext")), 223 | PlaintextCrc32c: int64(1), 224 | }, 225 | }, 226 | { 227 | name: "plaintext_crc32c missing", 228 | decryptResponse: &cloudkms.DecryptResponse{ 229 | Plaintext: base64.StdEncoding.EncodeToString([]byte("plaintext")), 230 | }, 231 | }, 232 | } 233 | 234 | for _, tc := range testcases { 235 | t.Run(tc.name, func(t *testing.T) { 236 | ctx := context.Background() 237 | ts, svc := initializeServerWithResponse(ctx, t, 238 | tc.decryptResponse) 239 | defer ts.Close() 240 | 241 | aead := newGCPAEAD("key name", svc) 242 | _, err := aead.Decrypt([]byte("ciphertext"), []byte("additional data")) 243 | if err == nil { 244 | t.Errorf("a.Decrypt err = nil, want error") 245 | } 246 | }) 247 | } 248 | } 249 | 250 | func TestDecrypt_Success(t *testing.T) { 251 | plaintext := []byte("plaintext") 252 | plaintextCrc32c := int64(crc32.Checksum(plaintext, crc32.MakeTable(crc32.Castagnoli))) 253 | 254 | ctx := context.Background() 255 | ts, svc := initializeServerWithResponse(ctx, t, 256 | &cloudkms.DecryptResponse{ 257 | Plaintext: base64.StdEncoding.EncodeToString(plaintext), 258 | PlaintextCrc32c: plaintextCrc32c, 259 | }) 260 | defer ts.Close() 261 | 262 | aead := newGCPAEAD("key name", svc) 263 | gotPlaintext, err := aead.Decrypt([]byte("ciphertext"), []byte("additional data")) 264 | if err != nil { 265 | t.Errorf("a.Decrypt err = %q, want nil", err) 266 | } 267 | if !bytes.Equal(gotPlaintext, plaintext) { 268 | t.Errorf("Returned plaitext: %q, want: %q", gotPlaintext, plaintext) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google LLC 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 provides integration with the [GCP Cloud KMS]. 16 | // 17 | // [GCP Cloud KMS]: https://cloud.google.com/kms/docs/quickstart. 18 | package gcpkms 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | "runtime" 25 | "strings" 26 | 27 | "cloud.google.com/go/kms/apiv1" 28 | "google.golang.org/api/cloudkms/v1" 29 | "google.golang.org/api/option" 30 | "github.com/tink-crypto/tink-go/v2/core/registry" 31 | "github.com/tink-crypto/tink-go/v2/tink" 32 | ) 33 | 34 | const ( 35 | gcpPrefix = "gcp-kms://" 36 | ) 37 | 38 | var ( 39 | errCred = errors.New("invalid credential path") 40 | tinkUserAgent = "Tink/" + tink.Version + " Golang/" + runtime.Version() 41 | ) 42 | 43 | // Transport indicates how the client should communicate with the KMS backend. 44 | type Transport int 45 | 46 | const ( 47 | // TransportGRPC communicates with Cloud KMS using gRPC. 48 | TransportGRPC Transport = 1 + iota 49 | // TransportREST communicates with Cloud KMS using the REST API via HTTP. 50 | TransportREST 51 | ) 52 | 53 | // Client represents a GCP KMS client. 54 | type Client struct { 55 | keyURIPrefix string 56 | restKMS *cloudkms.Service 57 | grpcKMS *kms.KeyManagementClient 58 | } 59 | 60 | var _ registry.KMSClient = (*Client)(nil) 61 | 62 | // NewClient returns a [Client] for the given key URI prefix. 63 | // 64 | // uriPrefix must have the following format: 65 | // 66 | // gcp-kms://[path] 67 | func NewClient(ctx context.Context, uriPrefix string, opts ...Option) (*Client, error) { 68 | if !strings.HasPrefix(strings.ToLower(uriPrefix), gcpPrefix) { 69 | return nil, fmt.Errorf("uriPrefix must start with %s", gcpPrefix) 70 | } 71 | var o options 72 | 73 | for _, opt := range DefaultOptions { 74 | opt(&o) 75 | } 76 | for _, opt := range opts { 77 | opt(&o) 78 | } 79 | 80 | o.apiOptions = append(o.apiOptions, option.WithUserAgent(tinkUserAgent)) 81 | 82 | c := &Client{keyURIPrefix: uriPrefix} 83 | 84 | switch o.transport { 85 | case TransportGRPC: 86 | kmsClient, err := kms.NewKeyManagementClient(ctx, o.apiOptions...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | c.grpcKMS = kmsClient 91 | case TransportREST: 92 | kmsService, err := cloudkms.NewService(ctx, o.apiOptions...) 93 | if err != nil { 94 | return nil, err 95 | } 96 | c.restKMS = kmsService 97 | default: 98 | return nil, fmt.Errorf("invalid transport specified: %v", o.transport) 99 | } 100 | 101 | return c, nil 102 | } 103 | 104 | // GetAEADWithContext returns a [tink.AEADWithContext] backed by keyURI. 105 | func GetAEADWithContext(ctx context.Context, keyURI string, opts ...Option) (tink.AEADWithContext, error) { 106 | c, err := NewClient(ctx, keyURI, opts...) 107 | if err != nil { 108 | return nil, err 109 | } 110 | if c.grpcKMS == nil { 111 | return nil, errors.New("AEADWithContext is only supported when using GRPC") 112 | } 113 | keyName := strings.TrimPrefix(keyURI, gcpPrefix) 114 | return newGRPCAEAD(keyName, c.grpcKMS), nil 115 | } 116 | 117 | // options holds the configuration options for a gcpkms.Client, including the transport protocol 118 | // and API client options. 119 | type options struct { 120 | transport Transport 121 | apiOptions []option.ClientOption 122 | } 123 | 124 | // Option is a functional option for configuring a gcpkms.Client. 125 | type Option func(*options) 126 | 127 | // WithTransport configures the transport protocol used by the gcpkms.Client. 128 | // 129 | // By default, [TransportGRPC] is used. 130 | func WithTransport(transport Transport) Option { 131 | return func(opts *options) { 132 | opts.transport = transport 133 | } 134 | } 135 | 136 | // WithGoogleAPIClientOptions configures the gcpkms.Client with Google API client options. 137 | func WithGoogleAPIClientOptions(apiOptions ...option.ClientOption) Option { 138 | return func(opts *options) { 139 | opts.apiOptions = apiOptions 140 | } 141 | } 142 | 143 | // DefaultOptions are the default configuration options for a [Client]. 144 | var DefaultOptions = []Option{ 145 | WithTransport(TransportGRPC), 146 | } 147 | 148 | // NewClientWithOptions returns a new [registry.KMSClient] with provided Google API 149 | // options to handle keys whose URI start with uriPrefix. 150 | // 151 | // uriPrefix must have the following format: 152 | // 153 | // gcp-kms://[path] 154 | // 155 | // This client uses [TransportREST] for communication with the GCP KMS backend. 156 | // 157 | // Deprecated: Use [NewClient] and [WithTransport] instead. 158 | func NewClientWithOptions(ctx context.Context, uriPrefix string, opts ...option.ClientOption) (registry.KMSClient, error) { 159 | return NewClient(ctx, uriPrefix, WithTransport(TransportREST), WithGoogleAPIClientOptions(opts...)) 160 | } 161 | 162 | // Supported returns true if this client does support keyURI. 163 | func (c *Client) Supported(keyURI string) bool { 164 | return strings.HasPrefix(keyURI, c.keyURIPrefix) 165 | } 166 | 167 | // GetAEAD gets an AEAD backend by keyURI. 168 | func (c *Client) GetAEAD(keyURI string) (tink.AEAD, error) { 169 | if !c.Supported(keyURI) { 170 | return nil, errors.New("unsupported keyURI") 171 | } 172 | 173 | keyName := strings.TrimPrefix(keyURI, gcpPrefix) 174 | 175 | switch { 176 | case c.grpcKMS != nil: 177 | return &aeadWithContextWrapper{AEADWithContext: newGRPCAEAD(keyName, c.grpcKMS)}, nil 178 | case c.restKMS != nil: 179 | return newGCPAEAD(keyName, c.restKMS), nil 180 | default: 181 | return nil, fmt.Errorf("no client present") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google LLC 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 | "bytes" 19 | "context" 20 | "log" 21 | 22 | "google.golang.org/api/option" 23 | "github.com/tink-crypto/tink-go/v2/aead" 24 | "github.com/tink-crypto/tink-go-gcpkms/v2/integration/gcpkms" 25 | ) 26 | 27 | func Example() { 28 | const keyURI = "gcp-kms://......" 29 | ctx := context.Background() 30 | // Replace "/mysecurestorage/credentials.json" with actual path or other auth method if needed for a real run. 31 | credentialsOpt := option.WithCredentialsFile("/mysecurestorage/credentials.json") 32 | 33 | // Get the KEK AEAD as AEADWithContext directly. 34 | kekAEAD, err := gcpkms.GetAEADWithContext(ctx, keyURI, gcpkms.WithGoogleAPIClientOptions(credentialsOpt)) 35 | if err != nil { 36 | log.Fatalf("gcpkms.GetAEADWithContext failed: %v", err) 37 | } 38 | 39 | // Create the KMS envelope AEAD primitive using AEADWithContext. 40 | dekTemplate := aead.AES128CTRHMACSHA256KeyTemplate() 41 | envelopeAEAD, err := aead.NewKMSEnvelopeAEADWithContext(dekTemplate, kekAEAD) 42 | if err != nil { 43 | log.Fatalf("aead.NewKMSEnvelopeAEADWithContext failed: %v", err) 44 | } 45 | 46 | // Use the primitive with context. 47 | plaintext := []byte("message for envelope with context") 48 | associatedData := []byte("example KMS envelope AEADWithContext encryption") 49 | 50 | ciphertext, err := envelopeAEAD.EncryptWithContext(ctx, plaintext, associatedData) 51 | if err != nil { 52 | log.Fatalf("envelopeAEAD.EncryptWithContext failed: %v", err) 53 | } 54 | 55 | decryptedPlaintext, err := envelopeAEAD.DecryptWithContext(ctx, ciphertext, associatedData) 56 | if err != nil { 57 | log.Fatalf("envelopeAEAD.DecryptWithContext failed: %v", err) 58 | } 59 | 60 | if !bytes.Equal(plaintext, decryptedPlaintext) { 61 | log.Fatalf("envelope decrypted text (%s) does not match original plaintext (%s)", decryptedPlaintext, plaintext) 62 | } 63 | } 64 | 65 | // Example_withoutContext demonstrates how to obtain a tink.AEAD from the GCP KMS client. 66 | // This approach is useful when working with APIs or parts of Tink that expect a tink.AEAD instance 67 | // rather than a tink.AEADWithContext. 68 | func Example_aeadWithoutContext() { 69 | const keyURI = "gcp-kms://......" 70 | ctx := context.Background() 71 | // Replace "/mysecurestorage/credentials.json" with actual path or other auth method if needed for a real run. 72 | credentialsOpt := option.WithCredentialsFile("/mysecurestorage/credentials.json") 73 | 74 | // Create a new GCP KMS client. 75 | // By default, NewClient uses gRPC. 76 | kmsClient, err := gcpkms.NewClient(ctx, keyURI, gcpkms.WithGoogleAPIClientOptions(credentialsOpt)) 77 | if err != nil { 78 | log.Fatalf("gcpkms.NewClient failed: %v", err) 79 | } 80 | 81 | // Get a regular tink.AEAD primitive from the client. 82 | // If the client is gRPC-based (default), this wraps the underlying AEADWithContext. 83 | regularAEAD, err := kmsClient.GetAEAD(keyURI) 84 | if err != nil { 85 | log.Fatalf("kmsClient.GetAEAD failed: %v", err) 86 | } 87 | 88 | // Use the tink.AEAD primitive. 89 | plaintext := []byte("message for regular AEAD") 90 | associatedData := []byte("example regular AEAD encryption") 91 | 92 | ciphertext, err := regularAEAD.Encrypt(plaintext, associatedData) 93 | if err != nil { 94 | log.Fatalf("regularAEAD.Encrypt failed: %v", err) 95 | } 96 | 97 | decryptedPlaintext, err := regularAEAD.Decrypt(ciphertext, associatedData) 98 | if err != nil { 99 | log.Fatalf("regularAEAD.Decrypt failed: %v", err) 100 | } 101 | 102 | if !bytes.Equal(plaintext, decryptedPlaintext) { 103 | log.Fatalf("regular AEAD decrypted text (%s) does not match original plaintext (%s)", decryptedPlaintext, plaintext) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_grpc_aead.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 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 | "strings" 21 | 22 | kmspb "cloud.google.com/go/kms/apiv1/kmspb" 23 | wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" 24 | "cloud.google.com/go/kms/apiv1" 25 | 26 | "github.com/tink-crypto/tink-go/v2/tink" 27 | ) 28 | 29 | // grpcAEAD represents a GCP GRPC-based KMS client to a particular URI. 30 | type grpcAEAD struct { 31 | keyURI string 32 | kms *kms.KeyManagementClient 33 | } 34 | 35 | var _ tink.AEADWithContext = (*grpcAEAD)(nil) 36 | 37 | // newGRPCAEAD returns a new GCP KMS client. 38 | func newGRPCAEAD(keyURI string, kms *kms.KeyManagementClient) tink.AEADWithContext { 39 | return &grpcAEAD{ 40 | keyURI: keyURI, 41 | kms: kms, 42 | } 43 | } 44 | 45 | // EncryptWithContext encrypts the plaintext with associatedData. 46 | func (a *grpcAEAD) EncryptWithContext(ctx context.Context, plaintext, associatedData []byte) ([]byte, error) { 47 | 48 | req := &kmspb.EncryptRequest{ 49 | Name: a.keyURI, 50 | Plaintext: plaintext, 51 | PlaintextCrc32C: wrapperspb.Int64(computeChecksum(plaintext)), 52 | AdditionalAuthenticatedData: associatedData, 53 | AdditionalAuthenticatedDataCrc32C: wrapperspb.Int64(computeChecksum(associatedData)), 54 | } 55 | 56 | resp, err := a.kms.Encrypt(ctx, req) 57 | 58 | if err != nil { 59 | return nil, err 60 | } 61 | if !resp.VerifiedPlaintextCrc32C { 62 | return nil, fmt.Errorf("KMS request for %q is missing the checksum field plaintext_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyURI) 63 | } 64 | if !resp.VerifiedAdditionalAuthenticatedDataCrc32C { 65 | return nil, fmt.Errorf("KMS request for %q is missing the checksum field additional_authenticated_data_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyURI) 66 | } 67 | if !strings.HasPrefix(resp.GetName(), a.keyURI) { 68 | return nil, fmt.Errorf("the requested key name %q does not match the key name in the KMS response %q", a.keyURI, resp.GetName()) 69 | } 70 | if resp.CiphertextCrc32C.GetValue() != computeChecksum(resp.Ciphertext) { 71 | return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field ciphertext_crc32c did not match the data in field ciphertext. Please retry in case this is a transient error", a.keyURI) 72 | } 73 | 74 | return resp.Ciphertext, nil 75 | } 76 | 77 | // DecryptWithContext decrypts ciphertext with associatedData. 78 | func (a *grpcAEAD) DecryptWithContext(ctx context.Context, ciphertext, associatedData []byte) ([]byte, error) { 79 | 80 | req := &kmspb.DecryptRequest{ 81 | Name: a.keyURI, 82 | Ciphertext: ciphertext, 83 | CiphertextCrc32C: wrapperspb.Int64(computeChecksum(ciphertext)), 84 | AdditionalAuthenticatedData: associatedData, 85 | AdditionalAuthenticatedDataCrc32C: wrapperspb.Int64(computeChecksum(associatedData)), 86 | } 87 | 88 | resp, err := a.kms.Decrypt(ctx, req) 89 | 90 | if err != nil { 91 | return nil, err 92 | } 93 | if resp.PlaintextCrc32C.GetValue() != computeChecksum(resp.Plaintext) { 94 | return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field plaintext_crc32c did not match the data in field plaintext. Please retry in case this is a transient error", a.keyURI) 95 | } 96 | 97 | return resp.Plaintext, nil 98 | } 99 | 100 | type aeadWithContextWrapper struct { 101 | tink.AEADWithContext 102 | } 103 | 104 | var _ tink.AEAD = (*aeadWithContextWrapper)(nil) 105 | 106 | func (w *aeadWithContextWrapper) Encrypt(plaintext, associatedData []byte) ([]byte, error) { 107 | return w.EncryptWithContext(context.TODO(), plaintext, associatedData) 108 | } 109 | 110 | func (w *aeadWithContextWrapper) Decrypt(ciphertext, associatedData []byte) ([]byte, error) { 111 | return w.DecryptWithContext(context.TODO(), ciphertext, associatedData) 112 | } 113 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_grpc_aead_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 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 | "bytes" 19 | "context" 20 | "hash/crc32" 21 | "net" 22 | "testing" 23 | 24 | kmspbgrpc "google.golang.org/genproto/googleapis/cloud/kms/v1" 25 | kmspb "cloud.google.com/go/kms/apiv1/kmspb" 26 | wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" 27 | "cloud.google.com/go/kms/apiv1" 28 | "google.golang.org/api/option" 29 | "google.golang.org/grpc/credentials/insecure" 30 | "google.golang.org/grpc" 31 | "google.golang.org/grpc/test/bufconn" 32 | ) 33 | 34 | // mockKMSService is a mock implementation of the KeyManagementServiceServer. 35 | type mockKMSService struct { 36 | kmspbgrpc.UnimplementedKeyManagementServiceServer 37 | encryptResponse *kmspb.EncryptResponse 38 | decryptResponse *kmspb.DecryptResponse 39 | } 40 | 41 | // Encrypt implements the server-side Encrypt RPC. 42 | func (m *mockKMSService) Encrypt(ctx context.Context, in *kmspb.EncryptRequest) (*kmspb.EncryptResponse, error) { 43 | return m.encryptResponse, nil 44 | } 45 | 46 | // Decrypt implements the server-side Decrypt RPC. 47 | func (m *mockKMSService) Decrypt(ctx context.Context, in *kmspb.DecryptRequest) (*kmspb.DecryptResponse, error) { 48 | return m.decryptResponse, nil 49 | } 50 | 51 | func initializeGRPCClientWithResponse(t *testing.T, encryptResponse *kmspb.EncryptResponse, decryptResponse *kmspb.DecryptResponse) *kms.KeyManagementClient { 52 | t.Helper() 53 | 54 | const bufSize = 1024 * 1024 55 | lis := bufconn.Listen(bufSize) 56 | s := grpc.NewServer() 57 | 58 | mockService := &mockKMSService{ 59 | encryptResponse: encryptResponse, 60 | decryptResponse: decryptResponse, 61 | } 62 | 63 | kmspbgrpc.RegisterKeyManagementServiceServer(s, mockService) 64 | 65 | go func() { 66 | if err := s.Serve(lis); err != nil { 67 | t.Logf("Mock gRPC server exited with error: %v", err) 68 | } 69 | }() 70 | 71 | t.Cleanup(func() { 72 | s.Stop() 73 | lis.Close() 74 | }) 75 | 76 | dialer := func(ctx context.Context, address string) (net.Conn, error) { 77 | return lis.Dial() 78 | } 79 | conn, err := grpc.NewClient("passthrough:///bufnet", grpc.WithContextDialer(dialer), grpc.WithTransportCredentials(insecure.NewCredentials())) 80 | 81 | if err != nil { 82 | t.Fatalf("failed to dial bufnet: %v", err) 83 | } 84 | t.Cleanup(func() { conn.Close() }) 85 | 86 | gcpKMSClient, err := kms.NewKeyManagementClient(t.Context(), option.WithGRPCConn(conn)) 87 | if err != nil { 88 | t.Fatalf("kms.NewKeyManagementClient with GRPCConn failed: %v", err) 89 | } 90 | 91 | return gcpKMSClient 92 | } 93 | 94 | func TestGRPCEncrypt_FailsWhenPlaintextUnverifed(t *testing.T) { 95 | additionalData := []byte("additional data") 96 | ciphertext := []byte("ciphertext") 97 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 98 | 99 | testcases := []struct { 100 | name string 101 | encryptResponse *kmspb.EncryptResponse 102 | }{ 103 | { 104 | name: "verified_plaintext_crc32c is false", 105 | encryptResponse: &kmspb.EncryptResponse{ 106 | Ciphertext: ciphertext, 107 | CiphertextCrc32C: wrapperspb.Int64(ciphertextCrc32c), 108 | VerifiedPlaintextCrc32C: false, 109 | VerifiedAdditionalAuthenticatedDataCrc32C: true, 110 | }, 111 | }, 112 | { 113 | name: "verified_plaintext_crc32c missing", 114 | encryptResponse: &kmspb.EncryptResponse{ 115 | Ciphertext: ciphertext, 116 | CiphertextCrc32C: wrapperspb.Int64(ciphertextCrc32c), 117 | VerifiedAdditionalAuthenticatedDataCrc32C: true, 118 | }, 119 | }, 120 | } 121 | 122 | for _, tc := range testcases { 123 | t.Run(tc.name, func(t *testing.T) { 124 | client := initializeGRPCClientWithResponse(t, tc.encryptResponse, nil) 125 | 126 | aead := newGRPCAEAD("key name", client) 127 | // Encryption should fail for all plaintexts (empty or non-empty). 128 | _, err := aead.EncryptWithContext(t.Context(), []byte("plaintext"), additionalData) 129 | if err == nil { 130 | t.Errorf("a.Encrypt err = nil, want error") 131 | } 132 | _, err = aead.EncryptWithContext(t.Context(), []byte(""), additionalData) 133 | if err == nil { 134 | t.Errorf("a.Encrypt err = nil, want error") 135 | } 136 | }) 137 | } 138 | } 139 | 140 | func TestGRPCEncrypt_FailsWhenAdditionalAuthenticatedDataUnverifed(t *testing.T) { 141 | plaintext := []byte("plaintext") 142 | ciphertext := []byte("ciphertext") 143 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 144 | 145 | testcases := []struct { 146 | name string 147 | encryptResponse *kmspb.EncryptResponse 148 | }{ 149 | { 150 | name: "verified_additional_authenticated_data_crc32c is false", 151 | encryptResponse: &kmspb.EncryptResponse{ 152 | Ciphertext: ciphertext, 153 | CiphertextCrc32C: wrapperspb.Int64(ciphertextCrc32c), 154 | VerifiedPlaintextCrc32C: true, 155 | VerifiedAdditionalAuthenticatedDataCrc32C: false, 156 | }, 157 | }, 158 | { 159 | name: "verified_additional_authenticated_data_crc32c missing", 160 | encryptResponse: &kmspb.EncryptResponse{ 161 | Ciphertext: ciphertext, 162 | CiphertextCrc32C: wrapperspb.Int64(ciphertextCrc32c), 163 | VerifiedPlaintextCrc32C: true, 164 | }, 165 | }, 166 | } 167 | 168 | for _, tc := range testcases { 169 | t.Run(tc.name, func(t *testing.T) { 170 | client := initializeGRPCClientWithResponse(t, tc.encryptResponse, nil) 171 | 172 | aead := newGRPCAEAD("key name", client) 173 | // Encryption should fail for all additional authenticated data (empty or non-empty). 174 | _, err := aead.EncryptWithContext(t.Context(), plaintext, []byte("additional data")) 175 | if err == nil { 176 | t.Errorf("a.Encrypt err = nil, want error") 177 | } 178 | _, err = aead.EncryptWithContext(t.Context(), plaintext, []byte("")) 179 | if err == nil { 180 | t.Errorf("a.Encrypt err = nil, want error") 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func TestGRPCEncrypt_FailsWithInvalidCiphertextCrc32c(t *testing.T) { 187 | testcases := []struct { 188 | name string 189 | encryptResponse *kmspb.EncryptResponse 190 | }{ 191 | { 192 | name: "ciphertext_crc32c does not match ciphertext", 193 | encryptResponse: &kmspb.EncryptResponse{ 194 | Ciphertext: []byte("ciphertext"), 195 | CiphertextCrc32C: wrapperspb.Int64(1), 196 | VerifiedPlaintextCrc32C: true, 197 | VerifiedAdditionalAuthenticatedDataCrc32C: true, 198 | }, 199 | }, 200 | { 201 | name: "ciphertext_crc32c missing", 202 | encryptResponse: &kmspb.EncryptResponse{ 203 | Ciphertext: []byte("ciphertext"), 204 | VerifiedPlaintextCrc32C: true, 205 | VerifiedAdditionalAuthenticatedDataCrc32C: true, 206 | }, 207 | }, 208 | } 209 | 210 | for _, tc := range testcases { 211 | t.Run(tc.name, func(t *testing.T) { 212 | client := initializeGRPCClientWithResponse(t, tc.encryptResponse, nil) 213 | 214 | aead := newGRPCAEAD("key name", client) 215 | _, err := aead.EncryptWithContext(t.Context(), []byte("plaintext"), []byte("additional data")) 216 | if err == nil { 217 | t.Errorf("a.Encrypt err = nil, want error") 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func TestGRPCEncrypt_Success(t *testing.T) { 224 | ciphertext := []byte("ciphertext") 225 | ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli))) 226 | 227 | client := initializeGRPCClientWithResponse(t, &kmspb.EncryptResponse{ 228 | Name: "key name", 229 | Ciphertext: ciphertext, 230 | CiphertextCrc32C: wrapperspb.Int64(ciphertextCrc32c), 231 | VerifiedPlaintextCrc32C: true, 232 | VerifiedAdditionalAuthenticatedDataCrc32C: true, 233 | }, nil) 234 | 235 | aead := newGRPCAEAD("key name", client) 236 | gotCiphertext, err := aead.EncryptWithContext(t.Context(), []byte("plaintext"), []byte("additional data")) 237 | if err != nil { 238 | t.Errorf("a.Encrypt err = %q, want nil", err) 239 | } 240 | if !bytes.Equal(gotCiphertext, ciphertext) { 241 | t.Errorf("Returned ciphertext: %q, want: %q", gotCiphertext, ciphertext) 242 | } 243 | } 244 | 245 | func TestGRPCDecrypt_FailsWithInvalidPlaintextCrc32c(t *testing.T) { 246 | testcases := []struct { 247 | name string 248 | decryptResponse *kmspb.DecryptResponse 249 | }{ 250 | { 251 | name: "plaintext_crc32c does not match plaintext", 252 | decryptResponse: &kmspb.DecryptResponse{ 253 | Plaintext: []byte("plaintext"), 254 | PlaintextCrc32C: wrapperspb.Int64(1), 255 | }, 256 | }, 257 | { 258 | name: "plaintext_crc32c missing", 259 | decryptResponse: &kmspb.DecryptResponse{ 260 | Plaintext: []byte("plaintext"), 261 | }, 262 | }, 263 | } 264 | 265 | for _, tc := range testcases { 266 | t.Run(tc.name, func(t *testing.T) { 267 | client := initializeGRPCClientWithResponse(t, nil, tc.decryptResponse) 268 | 269 | aead := newGRPCAEAD("key name", client) 270 | _, err := aead.DecryptWithContext(t.Context(), []byte("ciphertext"), []byte("additional data")) 271 | if err == nil { 272 | t.Errorf("a.Decrypt err = nil, want error") 273 | } 274 | }) 275 | } 276 | } 277 | 278 | func TestGRPCDecrypt_Success(t *testing.T) { 279 | plaintext := []byte("plaintext") 280 | plaintextCrc32c := int64(crc32.Checksum(plaintext, crc32.MakeTable(crc32.Castagnoli))) 281 | 282 | client := initializeGRPCClientWithResponse(t, nil, &kmspb.DecryptResponse{ 283 | Plaintext: plaintext, 284 | PlaintextCrc32C: wrapperspb.Int64(plaintextCrc32c), 285 | }) 286 | 287 | aead := newGRPCAEAD("key name", client) 288 | gotPlaintext, err := aead.DecryptWithContext(t.Context(), []byte("ciphertext"), []byte("additional data")) 289 | if err != nil { 290 | t.Errorf("a.Decrypt err = %q, want nil", err) 291 | } 292 | if !bytes.Equal(gotPlaintext, plaintext) { 293 | t.Errorf("Returned plaintext: %q, want: %q", gotPlaintext, plaintext) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /integration/gcpkms/gcp_kms_integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 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 | "bytes" 19 | "context" 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | // Placeholder for internal flag import. 25 | // context is used to cancel outstanding requests 26 | "google.golang.org/api/option" 27 | "github.com/tink-crypto/tink-go/v2/aead" 28 | "github.com/tink-crypto/tink-go-gcpkms/v2/integration/gcpkms" 29 | ) 30 | 31 | const ( 32 | keyURI = "gcp-kms://projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key" 33 | ) 34 | 35 | var ( 36 | credFile = "testdata/gcp/credential.json" 37 | ) 38 | 39 | // Placeholder for internal initialization. 40 | 41 | func testFilePath(t *testing.T, filename string) string { 42 | t.Helper() 43 | srcDir, ok := os.LookupEnv("TEST_SRCDIR") 44 | if ok { 45 | workspaceDir, ok := os.LookupEnv("TEST_WORKSPACE") 46 | if !ok { 47 | t.Fatal("TEST_WORKSPACE not found") 48 | } 49 | return filepath.Join(srcDir, workspaceDir, filename) 50 | } 51 | return filepath.Join("../..", filename) 52 | } 53 | 54 | func TestGetAeadWithEnvelopeAead(t *testing.T) { 55 | ctx := context.Background() 56 | gcpClient, err := gcpkms.NewClientWithOptions(ctx, keyURI, option.WithCredentialsFile(testFilePath(t, credFile))) 57 | if err != nil { 58 | t.Fatalf("gcpkms.NewClientWithOptions() err = %q, want nil", err) 59 | } 60 | kekAEAD, err := gcpClient.GetAEAD(keyURI) 61 | if err != nil { 62 | t.Fatalf("gcpClient.GetAEAD(keyURI) err = %q, want nil", err) 63 | } 64 | 65 | dekTemplate := aead.AES128CTRHMACSHA256KeyTemplate() 66 | a := aead.NewKMSEnvelopeAEAD2(dekTemplate, kekAEAD) 67 | if err != nil { 68 | t.Fatalf("a.Encrypt(plaintext, associatedData) err = %q, want nil", err) 69 | } 70 | plaintext := []byte("message") 71 | associatedData := []byte("example KMS envelope AEAD encryption") 72 | 73 | ciphertext, err := a.Encrypt(plaintext, associatedData) 74 | if err != nil { 75 | t.Fatalf("a.Encrypt(plaintext, associatedData) err = %q, want nil", err) 76 | } 77 | gotPlaintext, err := a.Decrypt(ciphertext, associatedData) 78 | if err != nil { 79 | t.Fatalf("a.Decrypt(ciphertext, associatedData) err = %q, want nil", err) 80 | } 81 | if !bytes.Equal(gotPlaintext, plaintext) { 82 | t.Errorf("a.Decrypt() = %q, want %q", gotPlaintext, plaintext) 83 | } 84 | 85 | _, err = a.Decrypt(ciphertext, []byte("invalid associatedData")) 86 | if err == nil { 87 | t.Error("a.Decrypt(ciphertext, []byte(\"invalid associatedData\")) err = nil, want error") 88 | } 89 | } 90 | 91 | func TestAeadWithTransportGRPC(t *testing.T) { 92 | ctx := context.Background() 93 | 94 | opts := []gcpkms.Option{ 95 | gcpkms.WithTransport(gcpkms.TransportGRPC), 96 | gcpkms.WithGoogleAPIClientOptions(option.WithCredentialsFile(testFilePath(t, credFile))), 97 | } 98 | gcpClient, err := gcpkms.NewClient(ctx, keyURI, opts...) 99 | if err != nil { 100 | t.Fatalf("gcpkms.NewClient() err = %q, want nil", err) 101 | } 102 | aead, err := gcpClient.GetAEAD(keyURI) 103 | if err != nil { 104 | t.Fatalf("gcpClient.GetAEAD(keyURI) err = %q, want nil", err) 105 | } 106 | 107 | testcases := []struct { 108 | name string 109 | plaintext []byte 110 | associatedData []byte 111 | }{ 112 | { 113 | name: "empty_plaintext", 114 | plaintext: []byte(""), 115 | associatedData: []byte("authenticated data"), 116 | }, 117 | { 118 | name: "empty_associated_data", 119 | plaintext: []byte("plaintext"), 120 | associatedData: []byte(""), 121 | }, 122 | } 123 | 124 | for _, tc := range testcases { 125 | t.Run(tc.name, func(t *testing.T) { 126 | ciphertext, err := aead.Encrypt(tc.plaintext, tc.associatedData) 127 | if err != nil { 128 | t.Fatalf("aead.EncryptWithContext(plaintext, associatedData) err = %q, want nil", err) 129 | } 130 | gotPlaintext, err := aead.Decrypt(ciphertext, tc.associatedData) 131 | if err != nil { 132 | t.Fatalf("aead.DecryptWithContext(ciphertext, associatedData) err = %q, want nil", err) 133 | } 134 | if !bytes.Equal(gotPlaintext, tc.plaintext) { 135 | t.Errorf("aead.DecryptWithContext() = %q, want %q", gotPlaintext, tc.plaintext) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestAeadCrossClient(t *testing.T) { 142 | ctx := context.Background() 143 | 144 | httpOpts := []gcpkms.Option{ 145 | gcpkms.WithTransport(gcpkms.TransportREST), 146 | gcpkms.WithGoogleAPIClientOptions(option.WithCredentialsFile(testFilePath(t, credFile))), 147 | } 148 | httpClient, err := gcpkms.NewClient(ctx, keyURI, httpOpts...) 149 | if err != nil { 150 | t.Fatalf("gcpkms.NewClient() err = %q, want nil", err) 151 | } 152 | httpAEAD, err := httpClient.GetAEAD(keyURI) 153 | if err != nil { 154 | t.Fatalf("httpClient.GetAEAD(keyURI) err = %q, want nil", err) 155 | } 156 | 157 | grpcOpts := []gcpkms.Option{ 158 | gcpkms.WithTransport(gcpkms.TransportGRPC), 159 | gcpkms.WithGoogleAPIClientOptions(option.WithCredentialsFile(testFilePath(t, credFile))), 160 | } 161 | grpcClient, err := gcpkms.NewClient(ctx, keyURI, grpcOpts...) 162 | if err != nil { 163 | t.Fatalf("gcpkms.NewClient() err = %q, want nil", err) 164 | } 165 | grpcAEAD, err := grpcClient.GetAEAD(keyURI) 166 | if err != nil { 167 | t.Fatalf("grpcClient.GetAEAD(keyURI) err = %q, want nil", err) 168 | } 169 | 170 | testcases := []struct { 171 | name string 172 | plaintext []byte 173 | associatedData []byte 174 | }{ 175 | { 176 | name: "empty_plaintext", 177 | plaintext: []byte(""), 178 | associatedData: []byte("authenticated data"), 179 | }, 180 | { 181 | name: "empty_associated_data", 182 | plaintext: []byte("plaintext"), 183 | associatedData: []byte(""), 184 | }, 185 | } 186 | 187 | for _, tc := range testcases { 188 | t.Run("encrypt_with_grpc_"+tc.name, func(t *testing.T) { 189 | ciphertext, err := grpcAEAD.Encrypt(tc.plaintext, tc.associatedData) 190 | if err != nil { 191 | t.Fatalf("grpcAEAD.Encrypt(plaintext, associatedData) err = %q, want nil", err) 192 | } 193 | gotPlaintext, err := httpAEAD.Decrypt(ciphertext, tc.associatedData) 194 | if err != nil { 195 | t.Fatalf("httpAEAD.Decrypt(ciphertext, associatedData) err = %q, want nil", err) 196 | } 197 | if !bytes.Equal(gotPlaintext, tc.plaintext) { 198 | t.Errorf("httpAEAD.Decrypt() = %q, want %q", gotPlaintext, tc.plaintext) 199 | } 200 | }) 201 | t.Run("encrypt_with_http_"+tc.name, func(t *testing.T) { 202 | ciphertext, err := httpAEAD.Encrypt(tc.plaintext, tc.associatedData) 203 | if err != nil { 204 | t.Fatalf("httpAEAD.Encrypt(plaintext, associatedData) err = %q, want nil", err) 205 | } 206 | gotPlaintext, err := grpcAEAD.Decrypt(ciphertext, tc.associatedData) 207 | if err != nil { 208 | t.Fatalf("grpcAEAD.Decrypt(ciphertext, associatedData) err = %q, want nil", err) 209 | } 210 | if !bytes.Equal(gotPlaintext, tc.plaintext) { 211 | t.Errorf("grpcAEAD.Decrypt() = %q, want %q", gotPlaintext, tc.plaintext) 212 | } 213 | }) 214 | } 215 | } 216 | 217 | func TestAead(t *testing.T) { 218 | ctx := context.Background() 219 | gcpClient, err := gcpkms.NewClientWithOptions(ctx, keyURI, option.WithCredentialsFile(testFilePath(t, credFile))) 220 | if err != nil { 221 | t.Fatalf("gcpkms.NewClientWithOptions() err = %q, want nil", err) 222 | } 223 | aead, err := gcpClient.GetAEAD(keyURI) 224 | if err != nil { 225 | t.Fatalf("gcpClient.GetAEAD(keyURI) err = %q, want nil", err) 226 | } 227 | 228 | testcases := []struct { 229 | name string 230 | plaintext []byte 231 | associatedData []byte 232 | }{ 233 | { 234 | name: "empty_plaintext", 235 | plaintext: []byte(""), 236 | associatedData: []byte("authenticated data"), 237 | }, 238 | { 239 | name: "empty_associated_data", 240 | plaintext: []byte("plaintext"), 241 | associatedData: []byte(""), 242 | }, 243 | } 244 | 245 | for _, tc := range testcases { 246 | t.Run(tc.name, func(t *testing.T) { 247 | ciphertext, err := aead.Encrypt(tc.plaintext, tc.associatedData) 248 | if err != nil { 249 | t.Fatalf("aead.Encrypt(plaintext, associatedData) err = %q, want nil", err) 250 | } 251 | gotPlaintext, err := aead.Decrypt(ciphertext, tc.associatedData) 252 | if err != nil { 253 | t.Fatalf("aead.Decrypt(ciphertext, associatedData) err = %q, want nil", err) 254 | } 255 | if !bytes.Equal(gotPlaintext, tc.plaintext) { 256 | t.Errorf("aead.Decrypt() = %q, want %q", gotPlaintext, tc.plaintext) 257 | } 258 | }) 259 | } 260 | } 261 | 262 | func TestAeadWithContext(t *testing.T) { 263 | ctx := context.Background() 264 | aeadWithContext, err := gcpkms.GetAEADWithContext(ctx, keyURI, gcpkms.WithGoogleAPIClientOptions(option.WithCredentialsFile(testFilePath(t, credFile)))) 265 | if err != nil { 266 | t.Fatalf("gcpkms.GetAEADWithContext() err = %q, want nil", err) 267 | } 268 | 269 | plaintext := []byte("message") 270 | associatedData := []byte("example context-aware encryption") 271 | 272 | ciphertext, err := aeadWithContext.EncryptWithContext(ctx, plaintext, associatedData) 273 | if err != nil { 274 | t.Fatalf("aeadWithContext.EncryptWithContext(ctx, plaintext, associatedData) err = %q, want nil", err) 275 | } 276 | 277 | gotPlaintext, err := aeadWithContext.DecryptWithContext(ctx, ciphertext, associatedData) 278 | 279 | if err != nil { 280 | t.Fatalf("aeadWithContext.DecryptWithContext(ctx, ciphertext, associatedData) err = %q, want nil", err) 281 | } 282 | if !bytes.Equal(gotPlaintext, plaintext) { 283 | t.Errorf("aeadWithContext.DecryptWithContext() = %q, want %q", gotPlaintext, plaintext) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /kokoro/create_github_release_branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | set -euo pipefail 18 | 19 | # Fail if RELEASE_VERSION is not set. 20 | if [[ -z "${RELEASE_VERSION:-}" ]]; then 21 | echo "RELEASE_VERSION must be set" >&2 22 | exit 1 23 | fi 24 | 25 | IS_KOKORO="false" 26 | if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then 27 | IS_KOKORO="true" 28 | fi 29 | readonly IS_KOKORO 30 | 31 | # If not defined, default to /tmp. 32 | : "${TMPDIR:="/tmp"}" 33 | 34 | # WARNING: Setting this environment varialble to "true" will cause this script 35 | # to actually perform a release. 36 | : "${DO_MAKE_RELEASE:="false"}" 37 | 38 | if [[ ! "${DO_MAKE_RELEASE}" =~ ^(false|true)$ ]]; then 39 | echo "DO_MAKE_RELEASE must be either \"true\" or \"false\"" >&2 40 | exit 1 41 | fi 42 | 43 | if [[ "${IS_KOKORO}" == "true" ]] ; then 44 | readonly TINK_BASE_DIR="$(echo "${KOKORO_ARTIFACTS_DIR}"/git*)" 45 | cd "${TINK_BASE_DIR}/tink_go_gcpkms" 46 | fi 47 | 48 | GITHUB_RELEASE_UTIL_OPTS=() 49 | if [[ "${IS_KOKORO}" == "true" ]] ; then 50 | # Note: KOKORO_GIT_COMMIT is populated by Kokoro. 51 | GITHUB_RELEASE_UTIL_OPTS+=( 52 | -c "${KOKORO_GIT_COMMIT}" 53 | -t "${GITHUB_ACCESS_TOKEN}" 54 | ) 55 | fi 56 | 57 | if [[ "${DO_MAKE_RELEASE}" == "true" ]]; then 58 | GITHUB_RELEASE_UTIL_OPTS+=( -r ) 59 | fi 60 | 61 | readonly GITHUB_RELEASE_UTIL_OPTS 62 | 63 | # If running on Kokoro, TMPDIR is populated with the tmp folder. 64 | readonly TMP_DIR="$(mktemp -d "${TMPDIR}/release_XXXXXX")" 65 | readonly RELEASE_UTIL_SCRIPT="$(pwd)/kokoro/testutils/github_release_util.sh" 66 | if [[ ! -f "${RELEASE_UTIL_SCRIPT}" ]]; then 67 | echo "${RELEASE_UTIL_SCRIPT} not found." 68 | echo "Make sure you run this script from the root of tink-go-gcpkms." 69 | return 1 70 | fi 71 | 72 | pushd "${TMP_DIR}" 73 | # Create a release branch. 74 | "${RELEASE_UTIL_SCRIPT}" "${GITHUB_RELEASE_UTIL_OPTS[@]}" create_branch \ 75 | "${RELEASE_VERSION}" tink-go-gcpkms 76 | popd 77 | -------------------------------------------------------------------------------- /kokoro/create_github_release_tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | set -euo pipefail 18 | 19 | # Fail if RELEASE_VERSION is not set. 20 | if [[ -z "${RELEASE_VERSION:-}" ]]; then 21 | echo "RELEASE_VERSION must be set" >&2 22 | exit 1 23 | fi 24 | 25 | IS_KOKORO="false" 26 | if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then 27 | IS_KOKORO="true" 28 | fi 29 | readonly IS_KOKORO 30 | 31 | # If not defined, default to /tmp. 32 | : "${TMPDIR:="/tmp"}" 33 | 34 | # WARNING: Setting this environment varialble to "true" will cause this script 35 | # to actually perform a release. 36 | : "${DO_MAKE_RELEASE:="false"}" 37 | 38 | if [[ ! "${DO_MAKE_RELEASE}" =~ ^(false|true)$ ]]; then 39 | echo "DO_MAKE_RELEASE must be either \"true\" or \"false\"" >&2 40 | exit 1 41 | fi 42 | 43 | if [[ "${IS_KOKORO}" == "true" ]] ; then 44 | readonly TINK_BASE_DIR="$(echo "${KOKORO_ARTIFACTS_DIR}"/git*)" 45 | cd "${TINK_BASE_DIR}/tink_go_gcpkms" 46 | fi 47 | 48 | GITHUB_RELEASE_UTIL_OPTS=() 49 | if [[ "${IS_KOKORO}" == "true" ]] ; then 50 | # Note: KOKORO_GIT_COMMIT is populated by Kokoro. 51 | GITHUB_RELEASE_UTIL_OPTS+=( 52 | -c "${KOKORO_GIT_COMMIT}" 53 | -t "${GITHUB_ACCESS_TOKEN}" 54 | ) 55 | fi 56 | 57 | if [[ "${DO_MAKE_RELEASE}" == "true" ]]; then 58 | GITHUB_RELEASE_UTIL_OPTS+=( -r ) 59 | fi 60 | 61 | readonly GITHUB_RELEASE_UTIL_OPTS 62 | 63 | # If running on Kokoro, TMPDIR is populated with the tmp folder. 64 | readonly TMP_DIR="$(mktemp -d "${TMPDIR}/release_XXXXXX")" 65 | readonly RELEASE_UTIL_SCRIPT="$(pwd)/kokoro/testutils/github_release_util.sh" 66 | if [[ ! -f "${RELEASE_UTIL_SCRIPT}" ]]; then 67 | echo "${RELEASE_UTIL_SCRIPT} not found." 68 | echo "Make sure you run this script from the root of tink-go-gcpkms." 69 | return 1 70 | fi 71 | 72 | pushd "${TMP_DIR}" 73 | # Create a release branch. 74 | "${RELEASE_UTIL_SCRIPT}" "${GITHUB_RELEASE_UTIL_OPTS[@]}" create_tag \ 75 | "${RELEASE_VERSION}" tink-go-gcpkms 76 | popd 77 | -------------------------------------------------------------------------------- /kokoro/gcp_ubuntu/gomod/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | # Builds and tests tink-go-gcpkms using Go tooling. 18 | # 19 | # The behavior of this script can be modified using the following optional env 20 | # variables: 21 | # 22 | # - CONTAINER_IMAGE (unset by default): By default when run locally this script 23 | # executes tests directly on the host. The CONTAINER_IMAGE variable can be set 24 | # to execute tests in a custom container image for local testing. E.g.: 25 | # 26 | # CONTAINER_IMAGE="us-docker.pkg.dev/tink-test-infrastructure/tink-ci-images/linux-tink-go-base:latest" \ 27 | # sh ./kokoro/gcp_ubuntu/gomod/run_tests.sh 28 | set -eEuo pipefail 29 | 30 | RUN_COMMAND_ARGS=() 31 | if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]]; then 32 | readonly TINK_BASE_DIR="$(echo "${KOKORO_ARTIFACTS_DIR}"/git*)" 33 | cd "${TINK_BASE_DIR}/tink_go_gcpkms" 34 | source "./kokoro/testutils/go_test_container_images.sh" 35 | CONTAINER_IMAGE="${TINK_GO_BASE_IMAGE}" 36 | RUN_COMMAND_ARGS+=( -k "${TINK_GCR_SERVICE_KEY}" ) 37 | fi 38 | readonly CONTAINER_IMAGE 39 | 40 | if [[ -n "${CONTAINER_IMAGE:-}" ]]; then 41 | RUN_COMMAND_ARGS+=( -c "${CONTAINER_IMAGE}" ) 42 | fi 43 | 44 | ./kokoro/testutils/docker_execute.sh "${RUN_COMMAND_ARGS[@]}" \ 45 | ./kokoro/testutils/check_go_generated_files_up_to_date.sh . 46 | ./kokoro/testutils/copy_credentials.sh "testdata" "gcp" 47 | 48 | cat < env_variables.txt 49 | GOARCH 50 | EOF 51 | 52 | RUN_COMMAND_ARGS+=( -e env_variables.txt ) 53 | 54 | readonly RUN_COMMAND_ARGS 55 | 56 | cat < _run_test.sh 57 | #!/bin/bash 58 | set -euo pipefail 59 | 60 | set -x 61 | go build -v ./... 62 | go test -v ./... 63 | EOF 64 | 65 | chmod +x _run_test.sh 66 | 67 | # Run cleanup on EXIT. 68 | trap cleanup EXIT 69 | 70 | cleanup() { 71 | rm -rf _run_test.sh 72 | rm -rf env_variables.txt 73 | } 74 | 75 | ./kokoro/testutils/docker_execute.sh "${RUN_COMMAND_ARGS[@]}" \ 76 | ./_run_test.sh 77 | -------------------------------------------------------------------------------- /kokoro/macos_external/gomod/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | set -euo pipefail 18 | 19 | if [[ -n "${KOKORO_ROOT:-}" ]]; then 20 | readonly TINK_BASE_DIR="$(echo "${KOKORO_ARTIFACTS_DIR}"/git*)" 21 | cd "${TINK_BASE_DIR}/tink_go_gcpkms" 22 | fi 23 | 24 | # Sourcing required to update callers environment. 25 | source ./kokoro/testutils/install_go.sh 26 | echo "Using go binary from $(which go): $(go version)" 27 | 28 | ./kokoro/testutils/copy_credentials.sh "testdata" "gcp" 29 | ( 30 | set -x 31 | go build -v ./... 32 | go test -v ./... 33 | ) 34 | -------------------------------------------------------------------------------- /kokoro/testutils/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//:__subpackages__"]) 2 | 3 | licenses(["notice"]) 4 | 5 | sh_binary( 6 | name = "test_utils", 7 | srcs = ["test_utils.sh"], 8 | ) 9 | 10 | sh_binary( 11 | name = "github_release_util", 12 | srcs = ["github_release_util.sh"], 13 | ) 14 | 15 | sh_test( 16 | name = "github_release_util_test", 17 | size = "small", 18 | srcs = ["github_release_util_test.sh"], 19 | args = [ 20 | "$(rlocationpath :github_release_util.sh)", 21 | "$(rlocationpath :test_utils)", 22 | ], 23 | data = [ 24 | ":github_release_util.sh", 25 | ":test_utils", 26 | ], 27 | target_compatible_with = select({ 28 | "@platforms//os:windows": ["@platforms//:incompatible"], 29 | "//conditions:default": [], 30 | }), 31 | ) 32 | -------------------------------------------------------------------------------- /kokoro/testutils/check_go_generated_files_up_to_date.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | # This scripts checks that a given Go workspace has its generated Bazel files up 18 | # to date. 19 | 20 | usage() { 21 | echo "Usage: $0 [-h] [-c ] " 22 | echo " -c: Value to pass to `-compat`. Default to 1.19." 23 | echo " -h: Help. Print this usage information." 24 | exit 1 25 | } 26 | 27 | COMPAT="1.19" 28 | GO_PROJECT_DIR= 29 | 30 | process_args() { 31 | # Parse options. 32 | while getopts "hc:" opt; do 33 | case "${opt}" in 34 | c) COMPAT="${OPTARG}" ;; 35 | *) usage ;; 36 | esac 37 | done 38 | shift $((OPTIND - 1)) 39 | readonly GO_PROJECT_DIR="$1" 40 | if [[ -z "${GO_PROJECT_DIR}" ]]; then 41 | usage 42 | fi 43 | } 44 | 45 | main() { 46 | process_args "$@" 47 | 48 | ( 49 | cd "${GO_PROJECT_DIR}" 50 | local -r temp_dir_current_generated_files="$(mktemp -dt \ 51 | current_tink_go_build_files.XXXXXX)" 52 | local -r go_generated_files=( 53 | ./go.mod 54 | ./go.sum 55 | ) 56 | 57 | # Copy all current generated files into temp_dir_current_generated_files. 58 | local -r current_go_generated_files=( "${go_generated_files[@]}" ) 59 | for generated_file_path in "${current_go_generated_files[@]}"; do 60 | mkdir -p \ 61 | "$(dirname \ 62 | "${temp_dir_current_generated_files}/${generated_file_path}")" 63 | cp "${generated_file_path}" \ 64 | "${temp_dir_current_generated_files}/${generated_file_path}" 65 | done 66 | 67 | # Update build files. 68 | go mod tidy -compat="${COMPAT}" 69 | 70 | # Compare current with new build files. 71 | local -r new_go_generated_files=( "${go_generated_files[@]}" ) 72 | 73 | for generated_file_path in "${new_go_generated_files[@]}"; do 74 | if ! cmp -s "${generated_file_path}" \ 75 | "${temp_dir_current_generated_files}/${generated_file_path}"; then 76 | echo "ERROR: ${generated_file_path} needs to be updated. Please follow \ 77 | the instructions on go/tink-workflows#update-go-build." >&2 78 | echo "Diff for ${generated_file_path}:" 79 | diff "${generated_file_path}" \ 80 | "${temp_dir_current_generated_files}/${generated_file_path}" 81 | exit 1 82 | fi 83 | done 84 | ) 85 | } 86 | 87 | main "$@" 88 | -------------------------------------------------------------------------------- /kokoro/testutils/copy_credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | #################################################################################### 17 | 18 | # This script takes credentials injected into the environment via the Kokoro job 19 | # configuration and copies them to the expected locations. 20 | # 21 | # The second argument indicates whether all KMS service credentials should be 22 | # copied (all) or only credentials for a specific KMS service (gcp|aws). 23 | # 24 | # Usage insructions: 25 | # 26 | # ./kokoro/testutils/copy_credentials.sh 27 | # 28 | 29 | TESTDATA_DIR= 30 | KMS_SERVICE= 31 | 32 | ####################################### 33 | # Process command line arguments. 34 | # 35 | # Globals: 36 | # TESTDATA_DIR 37 | # KMS_SERVICE 38 | ####################################### 39 | process_args() { 40 | TESTDATA_DIR="$1" 41 | readonly TESTDATA_DIR 42 | KMS_SERVICE="$2" 43 | readonly KMS_SERVICE 44 | 45 | if [[ -z "${TESTDATA_DIR}" ]]; then 46 | echo "Testdata directory must be set" >&2 47 | exit 1 48 | fi 49 | 50 | if [[ ! -d "${TESTDATA_DIR}" ]]; then 51 | echo "Testdata directory \"${TESTDATA_DIR}\" doesn't exist" >&2 52 | exit 1 53 | fi 54 | 55 | if [[ -z "${KMS_SERVICE}" ]]; then 56 | echo "KMS service must be specified" >&2 57 | exit 1 58 | fi 59 | } 60 | 61 | ####################################### 62 | # Copy GCP credentials. 63 | # 64 | # Globals: 65 | # TESTDATA_DIR 66 | # TINK_TEST_SERVICE_ACCOUNT 67 | ####################################### 68 | copy_gcp_credentials() { 69 | if [[ -z "${TINK_TEST_SERVICE_ACCOUNT}" ]]; then 70 | echo "ERROR: TINK_TEST_SERVICE_ACCOUNT is expected to be set" >&2 71 | exit 1 72 | fi 73 | cp "${TINK_TEST_SERVICE_ACCOUNT}" "${TESTDATA_DIR}/gcp/credential.json" 74 | } 75 | 76 | ####################################### 77 | # Copy AWS credentials. 78 | # 79 | # Globals: 80 | # TESTDATA_DIR 81 | # AWS_TINK_TEST_SERVICE_ACCOUNT 82 | ####################################### 83 | copy_aws_credentials() { 84 | if [[ -z "${AWS_TINK_TEST_SERVICE_ACCOUNT}" ]]; then 85 | echo "ERROR: AWS_TINK_TEST_SERVICE_ACCOUNT is expected to be set" >&2 86 | exit 1 87 | fi 88 | 89 | # Create the different format for the AWS credentials 90 | local -r aws_key_id="AKIATNYZMJOHVMN7MSYH" 91 | local -r aws_key="$(cat ${AWS_TINK_TEST_SERVICE_ACCOUNT})" 92 | 93 | cat < "${TESTDATA_DIR}/aws/credentials.ini" 94 | [default] 95 | aws_access_key_id = ${aws_key_id} 96 | aws_secret_access_key = ${aws_key} 97 | END 98 | 99 | cat < "${TESTDATA_DIR}/aws/credentials.cred" 100 | [default] 101 | accessKey = ${aws_key_id} 102 | secretKey = ${aws_key} 103 | END 104 | 105 | cat < "${TESTDATA_DIR}/aws/credentials.csv" 106 | User name,Password,Access key ID,Secret access key,Console login link 107 | tink-user1,,${aws_key_id},${aws_key},https://235739564943.signin.aws.amazon.com/console 108 | END 109 | } 110 | 111 | main() { 112 | if [[ -z "${KOKORO_ROOT}" ]]; then 113 | echo "Not running on Kokoro, skipping copying credentials." 114 | exit 0 115 | fi 116 | 117 | process_args "$@" 118 | 119 | case "${KMS_SERVICE}" in 120 | aws) 121 | copy_aws_credentials 122 | ;; 123 | gcp) 124 | copy_gcp_credentials 125 | ;; 126 | all) 127 | copy_aws_credentials 128 | copy_gcp_credentials 129 | ;; 130 | *) 131 | echo "Invalid KMS service \"${KMS_SERVICE}\"" >&2 132 | exit 1 133 | esac 134 | } 135 | 136 | main "$@" 137 | -------------------------------------------------------------------------------- /kokoro/testutils/docker_execute.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | # Utility script to run a command in a new docker container. 18 | # 19 | # This script must be run from inside the Tink library to run the command for. 20 | # 21 | # NOTE: When running in a new container, this script mounts the parent folder of 22 | # `pwd`. Other dependencies, if any, are assumed to be located there. For 23 | # example, if running tink-py tests, this script assumes: 24 | # - pwd => /path/to/parent/tink-py 25 | # - mount path => /path/to/parent 26 | # - ls /path/to/parent => tink_cc tink_py. 27 | 28 | set -eo pipefail 29 | 30 | usage() { 31 | cat <] [-k ] 33 | -c: [Optional] Container image to run the command on. 34 | -k: [Optional] Service key file path for pulling the image from the Google Artifact Registry (https://cloud.google.com/artifact-registry). 35 | -e: [Optional] File containing a list of environment variables to pass to Docker using --env-file (see https://docs.docker.com/engine/reference/commandline/run/#env). 36 | -m: [Optional] Additional --mount argument to pass to the Docker command (see https://docs.docker.com/reference/cli/docker/container/run/#mount). 37 | -h: Help. Print this usage information. 38 | EOF 39 | exit 1 40 | } 41 | 42 | # Args. 43 | COMMAND= 44 | 45 | # Options. 46 | CONTAINER_IMAGE_NAME= 47 | GCR_SERVICE_KEY_PATH= 48 | DOCKER_ENV_FILE= 49 | DOCKER_MOUNT_ARG= 50 | 51 | ####################################### 52 | # Process command line arguments. 53 | ####################################### 54 | process_args() { 55 | # Parse options. 56 | while getopts "hc:k:e:m:" opt; do 57 | case "${opt}" in 58 | c) CONTAINER_IMAGE_NAME="${OPTARG}" ;; 59 | k) GCR_SERVICE_KEY_PATH="${OPTARG}" ;; 60 | e) DOCKER_ENV_FILE="${OPTARG}" ;; 61 | m) DOCKER_MOUNT_ARG="${OPTARG}" ;; 62 | *) usage ;; 63 | esac 64 | done 65 | shift $((OPTIND - 1)) 66 | readonly CONTAINER_IMAGE_NAME 67 | readonly GCR_SERVICE_KEY_PATH 68 | readonly DOCKER_ENV_FILE 69 | readonly DOCKER_MOUNT_ARG 70 | readonly COMMAND=("$@") 71 | } 72 | 73 | main() { 74 | process_args "$@" 75 | 76 | if [[ -z "${CONTAINER_IMAGE_NAME:-}" ]]; then 77 | echo "Running command on the host" 78 | time "${COMMAND[@]}" 79 | else 80 | echo "Running command on a new container from image ${CONTAINER_IMAGE_NAME}" 81 | if [[ ! -z "${GCR_SERVICE_KEY_PATH:-}" ]]; then 82 | # Activate service account to read from a private artifact registry repo. 83 | gcloud auth activate-service-account --key-file="${GCR_SERVICE_KEY_PATH}" 84 | gcloud config set project tink-test-infrastructure 85 | gcloud auth configure-docker us-docker.pkg.dev --quiet 86 | fi 87 | local -r path_to_mount="$(dirname "$(pwd)")" 88 | local -r library_to_test="$(basename "$(pwd)")" 89 | time docker pull "${CONTAINER_IMAGE_NAME}" 90 | 91 | local docker_opts=( 92 | --network="host" 93 | --mount type=bind,src="${path_to_mount}",dst=/deps 94 | --workdir=/deps/"${library_to_test}" 95 | --rm 96 | ) 97 | if [[ -n "${DOCKER_ENV_FILE}" ]]; then 98 | docker_opts+=( --env-file="${DOCKER_ENV_FILE}" ) 99 | fi 100 | if [[ -n ${DOCKER_MOUNT_ARG} ]] ; then 101 | docker_opts+=( --mount "${DOCKER_MOUNT_ARG}" ) 102 | fi 103 | readonly docker_opts 104 | time docker run "${docker_opts[@]}" "${CONTAINER_IMAGE_NAME}" \ 105 | bash -c "$(echo "${COMMAND[@]}")" 106 | fi 107 | } 108 | 109 | main "$@" 110 | -------------------------------------------------------------------------------- /kokoro/testutils/github_release_util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | # This script performs a source release on GitHub for a given repo, that is: 18 | # - Creates a release branch (if it does not yet exist), 19 | # - Creates a release tag. 20 | set -eo pipefail 21 | 22 | # Parameters and arguments. These will be populated/modified by args parsing. 23 | 24 | # Options. 25 | # Whether to actually create a release. This is false by default and meant to 26 | # prevent accidental releases. 27 | DO_RUN_ACTION="false" 28 | # Commit at which to make the release. If unspecified, the release is made from 29 | # HEAD. 30 | COMMIT_HASH= 31 | # Optional personal access token. 32 | ACCESS_TOKEN= 33 | 34 | # Arguments. 35 | # Action to be performed. 36 | ACTION= 37 | # This must be of the form `MAJOR.MINOR.PATCH`. 38 | VERSION= 39 | # Repo name after github.com/tink-crypto/, e.g., tink-cc. 40 | REPO_NAME= 41 | 42 | # Derived variables. 43 | GITHUB_REPO_URL= 44 | RELEASE_BRANCH= 45 | TAG= 46 | GITHUB_REFS= 47 | BRANCH_EXISTS="false" 48 | 49 | # Constants. 50 | readonly GITHUB_ORG_URL="github.com/tink-crypto" 51 | 52 | usage() { 53 | echo "Usage: $0 [-rh] [-c ] [-t ] \\" 54 | echo " " 55 | echo " : The action to be performed (crete_branch|create_tag)." 56 | echo " : The version identifier in MAJOR.MINOR.PATCH format." 57 | echo " : The name of the repository (e.g. \"tink-cc\")." 58 | echo " -c: Commit hash to use as HEAD of the release branch (optional)." 59 | echo " -t: Access token. Without this, the default is SSH (optional)." 60 | echo " -r: Whether to actually create a release; this is false by default." 61 | echo " -h: Show this help message." 62 | exit 1 63 | } 64 | 65 | process_params() { 66 | while getopts "rhc:t:" opt; do 67 | case "${opt}" in 68 | r) DO_RUN_ACTION="true" ;; 69 | c) COMMIT_HASH="${OPTARG}" ;; 70 | t) ACCESS_TOKEN="${OPTARG}" ;; 71 | *) usage ;; 72 | esac 73 | done 74 | shift $((OPTIND - 1)) 75 | readonly DO_RUN_ACTION 76 | readonly COMMIT_HASH 77 | readonly ACCESS_TOKEN 78 | 79 | ACTION="$1" 80 | if [[ ! "${ACTION}" =~ create_branch|create_tag ]]; then 81 | echo "ERROR: Expected (create_branch|create_tag) got ${ACTION}" >&2 82 | usage 83 | fi 84 | readonly ACTION 85 | 86 | VERSION="$2" 87 | if [[ ! "${VERSION}" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then 88 | echo "ERROR: Invalid version format: expected MAJOR.MINOR.PATCH, got \ 89 | ${VERSION}" >&2 90 | usage 91 | fi 92 | readonly VERSION 93 | 94 | REPO_NAME="$3" 95 | if [[ -z "${REPO_NAME}" ]]; then 96 | echo "ERROR: Repo name must be specified." >&2 97 | usage 98 | fi 99 | readonly REPO_NAME 100 | 101 | # Use SSH by default. 102 | local protocol_and_credentials="ssh://git" 103 | if [[ -n "${ACCESS_TOKEN}" ]]; then 104 | protocol_and_credentials="https://ise-crypto:${ACCESS_TOKEN}" 105 | fi 106 | readonly protocol_and_credentials 107 | GITHUB_REPO_URL="${protocol_and_credentials}@${GITHUB_ORG_URL}/${REPO_NAME}" 108 | readonly GITHUB_REPO_URL 109 | 110 | # Release branch is only MAJOR.MINOR. 111 | readonly RELEASE_BRANCH="$(echo "${VERSION}" | cut -d'.' -f1,2)" 112 | 113 | # Splitting declaration and assignment guarantees correct propagation of the 114 | # exit code of the subshell. 115 | local GITHUB_REFS 116 | GITHUB_REFS="$(git ls-remote "${GITHUB_REPO_URL}")" 117 | readonly GITHUB_REFS 118 | 119 | local -r expected_release_branch="refs/heads/${RELEASE_BRANCH}" 120 | if echo "${GITHUB_REFS}" | grep "${expected_release_branch}" > /dev/null; then 121 | BRANCH_EXISTS="true" 122 | fi 123 | readonly BRANCH_EXISTS 124 | 125 | if [[ "${ACTION}" == "create_tag" ]]; then 126 | if [[ "${BRANCH_EXISTS}" == "false" ]]; then 127 | echo "ERROR: The release branch does not exist in \ 128 | ${GITHUB_ORG_URL}/${REPO_NAME}." >&2 129 | return 1 130 | fi 131 | local -r release_tag="v${VERSION}" 132 | local -r expected_release_tag="refs/tags/${release_tag}" 133 | if echo "${GITHUB_REFS}" | grep "${expected_release_tag}" > /dev/null; then 134 | echo "ERROR The tag \"${release_tag}\" already exists in \ 135 | ${GITHUB_ORG_URL}/${REPO_NAME}." >&2 136 | return 1 137 | fi 138 | 139 | fi 140 | } 141 | 142 | ####################################### 143 | # Prints a command 144 | # 145 | # Args: 146 | # Command to execute. 147 | # 148 | ####################################### 149 | print_command() { 150 | printf '%q ' '+' "$@" 151 | echo 152 | } 153 | 154 | ####################################### 155 | # Runs a command if DO_RUN_ACTION is true. 156 | # 157 | # Args: 158 | # Command to execute. 159 | # Globals: 160 | # DO_RUN_ACTION 161 | # 162 | ####################################### 163 | run_command() { 164 | if [[ "${DO_RUN_ACTION}" == "false" ]]; then 165 | echo " *** Dry run, command not executed. ***" 166 | return 0 167 | fi 168 | # Actually run the command. 169 | "$@" 170 | return $? 171 | } 172 | 173 | ####################################### 174 | # Prints and runs a command. 175 | # 176 | # Args: 177 | # Command to execute. 178 | # 179 | ####################################### 180 | print_and_run_command() { 181 | print_command "$@" 182 | run_command "$@" 183 | } 184 | 185 | ####################################### 186 | # Creates and checks out to the release branch. 187 | # 188 | # If COMMIT_HASH is specified, use COMMIT_HASH as HEAD for the branch. 189 | # 190 | # Globals: 191 | # RELEASE_BRANCH 192 | # COMMIT_HASH 193 | # 194 | ####################################### 195 | git_create_release_branch() { 196 | if [[ "${BRANCH_EXISTS}" == "true" ]]; then 197 | echo "WARNING: The release branch already exists. Nothing to do." 198 | return 0 199 | fi 200 | # Target branch does not exist so we create the release branch. 201 | if [[ -n "${COMMIT_HASH:-}" ]]; then 202 | # Use COMMIT_HASH as HEAD for this branch. 203 | print_and_run_command git branch "${RELEASE_BRANCH}" "${COMMIT_HASH}" 204 | else 205 | print_and_run_command git branch "${RELEASE_BRANCH}" 206 | fi 207 | print_and_run_command git push origin "${RELEASE_BRANCH}" 208 | } 209 | 210 | ####################################### 211 | # Creates a release tag. 212 | # 213 | # Globals: 214 | # RELEASE_BRANCH 215 | # REPO_NAME 216 | # VERSION 217 | # 218 | ####################################### 219 | git_create_release_tag() { 220 | if [[ "${BRANCH_EXISTS}" == "false" ]]; then 221 | echo "ERROR: The release branch does not exist in \ 222 | ${GITHUB_ORG_URL}/${REPO_NAME}." >&2 223 | return 1 224 | fi 225 | local -r release_tag="v${VERSION}" 226 | local -r expected_release_tag="refs/tags/${release_tag}" 227 | if echo "${GITHUB_REFS}" | grep "${expected_release_tag}" > /dev/null; then 228 | echo "ERROR The tag \"${release_tag}\" already exists in \ 229 | ${GITHUB_ORG_URL}/${REPO_NAME}." >&2 230 | return 1 231 | fi 232 | print_and_run_command git checkout "${RELEASE_BRANCH}" 233 | print_and_run_command git tag -a "${release_tag}" \ 234 | -m "${REPO_NAME} version ${VERSION}" 235 | print_and_run_command git push origin "${release_tag}" 236 | } 237 | 238 | main() { 239 | process_params "$@" 240 | # Avoid logging the full URL; replace GIT_URL with a version that omits user 241 | # and access token. 242 | local -r protocol="$(echo "${GITHUB_REPO_URL}" | cut -d':' -f1)" 243 | local -r github_repo="$(echo "${GITHUB_REPO_URL}" | cut -d'@' -f2)" 244 | print_command git clone "${protocol}://...@${github_repo}" 245 | run_command git clone "${GITHUB_REPO_URL}" 246 | print_and_run_command cd "${REPO_NAME}" 247 | 248 | case "${ACTION}" in 249 | create_branch) git_create_release_branch ;; 250 | create_tag) git_create_release_tag ;; 251 | esac 252 | } 253 | 254 | main "$@" 255 | -------------------------------------------------------------------------------- /kokoro/testutils/github_release_util_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | DEFAULT_DIR="$(pwd)" 18 | if [[ -n "${TEST_SRCDIR}" ]]; then 19 | DEFAULT_DIR="${TEST_SRCDIR}" 20 | fi 21 | readonly DEFAULT_DIR 22 | readonly CLI="${DEFAULT_DIR}/${1:-"github_release_util.sh"}" 23 | readonly TEST_UTILS="${DEFAULT_DIR}/${2:-test_utils.sh}" 24 | 25 | # Load the test library. 26 | source "${TEST_UTILS}" 27 | 28 | test_GitHubReleaseUtil_CreateBranchMinorSucceeds() { 29 | cd "${TEST_CASE_TMPDIR}" 30 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 31 | cat << EOF > ${expected_git_cmds_file} 32 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 33 | git clone ssh://git@github.com/tink-crypto/some-repo 34 | git branch 1.6 35 | git push origin 1.6 36 | EOF 37 | 38 | local -r actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_git_cmds_file.txt" 39 | # Mock git command. 40 | git() { 41 | local -r command="$1" 42 | shift 1 43 | cmd_and_args="git ${command} $@" 44 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 45 | case "${command}" in 46 | "ls-remote") 47 | cat << EOF 48 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 49 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 50 | EOF 51 | ;; 52 | "clone") 53 | local -r repo_name="${1##*/}" 54 | mkdir "${repo_name}" 55 | ;; 56 | *) ;; # Do nothing 57 | esac 58 | } 59 | # Run this in the caller's environment. 60 | ( 61 | source "${CLI}" -r create_branch 1.6.0 some-repo &> /dev/null 62 | ) 63 | ASSERT_CMD_SUCCEEDED 64 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 65 | } 66 | 67 | test_GitHubReleaseUtil_CreateBranchMinorWithCommitSucceeds() { 68 | cd "${TEST_CASE_TMPDIR}" 69 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 70 | cat << EOF > ${expected_git_cmds_file} 71 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 72 | git clone ssh://git@github.com/tink-crypto/some-repo 73 | git branch 1.6 6c68b48c884e0aeb983b8864f35187d9584d0d74 74 | git push origin 1.6 75 | EOF 76 | local -r actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_git_cmds_file.txt" 77 | # Mock git command. 78 | git() { 79 | local -r command="$1" 80 | shift 1 81 | cmd_and_args="git ${command} $@" 82 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 83 | case "${command}" in 84 | "ls-remote") 85 | cat << EOF 86 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 87 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 88 | EOF 89 | ;; 90 | "clone") 91 | local -r repo_name="${1##*/}" 92 | mkdir "${repo_name}" 93 | ;; 94 | *) ;; # Do nothing 95 | esac 96 | } 97 | 98 | # Run this in the caller's environment. 99 | ( 100 | source "${CLI}" -r -c 6c68b48c884e0aeb983b8864f35187d9584d0d74 \ 101 | create_branch 1.6.0 some-repo &> /dev/null 102 | ) 103 | ASSERT_CMD_SUCCEEDED 104 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 105 | } 106 | 107 | test_GitHubReleaseUtil_CreateTagPatchSucceeds() { 108 | cd "${TEST_CASE_TMPDIR}" 109 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 110 | cat << EOF > ${expected_git_cmds_file} 111 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 112 | git clone ssh://git@github.com/tink-crypto/some-repo 113 | git checkout 1.6 114 | git tag -a v1.6.2 -m some-repo version 1.6.2 115 | git push origin v1.6.2 116 | EOF 117 | local -r actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_git_cmds_file.txt" 118 | # Mock git command. 119 | git() { 120 | local -r command="$1" 121 | shift 1 122 | cmd_and_args="git ${command} $@" 123 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 124 | case "${command}" in 125 | "ls-remote") 126 | cat << EOF 127 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 128 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 129 | 9940095f3081a116fa7a1337ad5ba27a3ccc59fe refs/heads/1.6 130 | EOF 131 | ;; 132 | "clone") 133 | local -r repo_name="${1##*/}" 134 | mkdir "${repo_name}" 135 | ;; 136 | *) ;; # Do nothing. 137 | esac 138 | } 139 | 140 | # Run this in the caller's environment. 141 | ( 142 | source "${CLI}" -r -c 6c68b48c884e0aeb983b8864f35187d9584d0d74 \ 143 | create_tag 1.6.2 some-repo &> /dev/null 144 | ) 145 | ASSERT_CMD_SUCCEEDED 146 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 147 | } 148 | 149 | test_GitHubReleaseUtil_CreateBranchFailsWhenLsRemoteFails() { 150 | cd "${TEST_CASE_TMPDIR}" 151 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 152 | cat << EOF > ${expected_git_cmds_file} 153 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 154 | EOF 155 | local actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_cmds.txt" 156 | # Mock git command. 157 | git() { 158 | local -r command="$1" 159 | shift 1 160 | cmd_and_args="git ${command} $@" 161 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 162 | case "${command}" in 163 | "ls-remote") return 1 ;; 164 | *) ;; # Do nothing. 165 | esac 166 | } 167 | 168 | # Run this in a subshell to prevent exiting on failure. 169 | ( 170 | source "${CLI}" -r create_branch 1.6.2 some-repo &> /dev/null 171 | ) 172 | ASSERT_CMD_FAILED 173 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 174 | echo "" > "${actual_git_cmds_file}" 175 | } 176 | 177 | test_GitHubReleaseUtil_CreateBranchFailsWhenCloneFails() { 178 | cd "${TEST_CASE_TMPDIR}" 179 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 180 | cat << EOF > ${expected_git_cmds_file} 181 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 182 | git clone ssh://git@github.com/tink-crypto/some-repo 183 | EOF 184 | local actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_cmds.txt" 185 | # Mock git command. 186 | git() { 187 | local -r command="$1" 188 | shift 1 189 | cmd_and_args="git ${command} $@" 190 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 191 | case "${command}" in 192 | "ls-remote") 193 | cat << EOF 194 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 195 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 196 | EOF 197 | ;; 198 | "clone") return 1 ;; 199 | *) ;; # Do nothing. 200 | esac 201 | } 202 | 203 | # Run this in a subshell to prevent exiting on failure. 204 | ( 205 | source "${CLI}" -r create_branch 1.6.2 some-repo &> /dev/null 206 | ) 207 | ASSERT_CMD_FAILED 208 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 209 | } 210 | 211 | test_GitHubReleaseUtil_CreateTagFailsIfBranchDoesNotExist() { 212 | cd "${TEST_CASE_TMPDIR}" 213 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 214 | cat << EOF > ${expected_git_cmds_file} 215 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 216 | EOF 217 | local actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_cmds.txt" 218 | # Mock git command. 219 | git() { 220 | local -r command="$1" 221 | shift 1 222 | cmd_and_args="git ${command} $@" 223 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 224 | case "${command}" in 225 | "ls-remote") 226 | cat << EOF 227 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 228 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 229 | EOF 230 | ;; 231 | *) ;; # Do nothing. 232 | esac 233 | } 234 | 235 | # Run this in a subshell to prevent exiting on failure. 236 | ( 237 | source "${CLI}" -r create_tag 1.6.2 some-repo &> /dev/null 238 | ) 239 | 240 | ASSERT_CMD_FAILED 241 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 242 | } 243 | 244 | test_GitHubReleaseUtil_CreateTagFailsIfReleaseTagAlreadyExists() { 245 | cd "${TEST_CASE_TMPDIR}" 246 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 247 | cat << EOF > ${expected_git_cmds_file} 248 | git ls-remote ssh://git@github.com/tink-crypto/some-repo 249 | EOF 250 | local actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_cmds.txt" 251 | # Mock git command. 252 | git() { 253 | local -r command="$1" 254 | shift 1 255 | cmd_and_args="git ${command} $@" 256 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 257 | case "${command}" in 258 | "ls-remote") 259 | cat << EOF 260 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 261 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 262 | 112a7d3a0453a1d926448519f94fe5a91c69be45 refs/heads/1.6 263 | 8c266441044c4dfaf7560e21663a8037043b750b refs/tags/v1.6.2 264 | 195ec3c1edeee8877ab5dc287f95c4402e3fb510 refs/tags/v1.6.1 265 | c6f48771296bca0bd22724b208abafeae7d7b764 refs/tags/v1.6.0 266 | EOF 267 | ;; 268 | *) ;; # Do nothing. 269 | esac 270 | } 271 | 272 | # Run this in a subshell to prevent exiting on failure. 273 | ( 274 | source "${CLI}" -r create_tag 1.6.2 some-repo &> /dev/null 275 | ) 276 | 277 | ASSERT_CMD_FAILED 278 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 279 | } 280 | 281 | test_GitHubReleaseUtil_FailsWhenInvalidVersion() { 282 | cd "${TEST_CASE_TMPDIR}" 283 | for action in create_branch create_tag; do 284 | ( 285 | source "${CLI}" -r "${action}" 1 some-repo &> /dev/null 286 | ) 287 | ASSERT_CMD_FAILED 288 | ( 289 | source "${CLI}" -r "${action}" 1.2 some-repo &> /dev/null 290 | ) 291 | ASSERT_CMD_FAILED 292 | ( 293 | source "${CLI}" -r "${action}" 1.2.a some-repo &> /dev/null 294 | ) 295 | ASSERT_CMD_FAILED 296 | ( 297 | source "${CLI}" -r "${action}" a.b.c some-repo &> /dev/null 298 | ) 299 | ASSERT_CMD_FAILED 300 | ( 301 | source "${CLI}" -r "${action}" 1.2.3.4 some-repo &> /dev/null 302 | ) 303 | ASSERT_CMD_FAILED 304 | ( 305 | source "${CLI}" -r "${action}" invalid some-repo &> /dev/null 306 | ) 307 | ASSERT_CMD_FAILED 308 | done 309 | } 310 | 311 | test_GitHubReleaseUtil_FailsWhenNoRepoNameIsGiven() { 312 | cd "${TEST_CASE_TMPDIR}" 313 | for action in create_branch create_tag; do 314 | # Run this in a subshell to prevent exiting on failure. 315 | ( 316 | source "${CLI}" -r "${action}" 1.6.0 &> /dev/null 317 | ) 318 | ASSERT_CMD_FAILED 319 | done 320 | } 321 | 322 | test_GitHubReleaseUtil_CreateBranchUsesCorrectGithubToken() { 323 | cd "${TEST_CASE_TMPDIR}" 324 | local -r access_token="a227da63673c236090a067c3f96b62e74dbd5857" 325 | local -r expected_url="https://ise-crypto:${access_token}@github.com/tink-crypto/some-repo" 326 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 327 | cat << EOF > ${expected_git_cmds_file} 328 | git ls-remote ${expected_url} 329 | git clone ${expected_url} 330 | git branch 1.6 331 | git push origin 1.6 332 | EOF 333 | local -r actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_git_cmds_file.txt" 334 | # Mock git command. 335 | git() { 336 | local -r command="$1" 337 | shift 1 338 | cmd_and_args="git ${command} $@" 339 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 340 | case "${command}" in 341 | "ls-remote") 342 | cat << EOF 343 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 344 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 345 | EOF 346 | ;; 347 | "clone") 348 | local -r repo_name="${1##*/}" 349 | mkdir "${repo_name}" 350 | ;; 351 | *) ;; # Do nothing 352 | esac 353 | } 354 | 355 | # Run this in the caller's environment. 356 | ( 357 | source "${CLI}" -r -t "${access_token}" create_branch 1.6.0 some-repo \ 358 | &> /dev/null 359 | ) 360 | ASSERT_CMD_SUCCEEDED 361 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 362 | } 363 | 364 | test_GitHubReleaseUtil_CreateTagUsesCorrectGithubToken() { 365 | cd "${TEST_CASE_TMPDIR}" 366 | local -r access_token="a227da63673c236090a067c3f96b62e74dbd5857" 367 | local -r expected_url="https://ise-crypto:${access_token}@github.com/tink-crypto/some-repo" 368 | local -r expected_git_cmds_file="${TEST_CASE_TMPDIR}/expected_cmds.txt" 369 | cat << EOF > ${expected_git_cmds_file} 370 | git ls-remote ${expected_url} 371 | git clone ${expected_url} 372 | git checkout 1.6 373 | git tag -a v1.6.2 -m some-repo version 1.6.2 374 | git push origin v1.6.2 375 | EOF 376 | local -r actual_git_cmds_file="${TEST_CASE_TMPDIR}/actual_git_cmds_file.txt" 377 | # Mock git command. 378 | git() { 379 | local -r command="$1" 380 | shift 1 381 | cmd_and_args="git ${command} $@" 382 | echo "${cmd_and_args}" >> "${actual_git_cmds_file}" 383 | case "${command}" in 384 | "ls-remote") 385 | cat << EOF 386 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 HEAD 387 | 6c68b48c884e0aeb983b8864f35187d9584d0d74 refs/heads/main 388 | 112a7d3a0453a1d926448519f94fe5a91c69be45 refs/heads/1.6 389 | EOF 390 | ;; 391 | "clone") 392 | local -r repo_name="${1##*/}" 393 | mkdir "${repo_name}" 394 | ;; 395 | *) ;; # Do nothing 396 | esac 397 | } 398 | 399 | # Run this in the caller's environment. 400 | ( 401 | source "${CLI}" -r -t "${access_token}" create_tag 1.6.2 some-repo \ 402 | &> /dev/null 403 | ) 404 | ASSERT_CMD_SUCCEEDED 405 | ASSERT_FILE_EQUALS "${actual_git_cmds_file}" "${expected_git_cmds_file}" 406 | } 407 | 408 | main() { 409 | run_all_tests "$@" 410 | } 411 | 412 | main "$@" 413 | -------------------------------------------------------------------------------- /kokoro/testutils/go_test_container_images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | _image_prefix() { 18 | local -r artifact_registry_url="us-docker.pkg.dev" 19 | local -r test_project="tink-test-infrastructure" 20 | local -r artifact_registry_repo="tink-ci-images" 21 | echo "${artifact_registry_url}/${test_project}/${artifact_registry_repo}" 22 | } 23 | 24 | # Linux container images for Tink Go libraries. 25 | readonly TINK_GO_BASE_IMAGE_NAME="linux-tink-go-base" 26 | # Image from 2024-09-04. 27 | readonly TINK_GO_BASE_IMAGE_HASH="db74fb6e70f3f06b3a409b44723953743731af4aa6318d2bf42c3225b4c58b20" 28 | readonly TINK_GO_BASE_IMAGE="$(_image_prefix)/${TINK_GO_BASE_IMAGE_NAME}@sha256:${TINK_GO_BASE_IMAGE_HASH}" 29 | 30 | unset -f _image_prefix 31 | -------------------------------------------------------------------------------- /kokoro/testutils/install_go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ################################################################################ 17 | 18 | # This script installs a recent version of Go into a temporary directory. The Go 19 | # bin directory is then added to the PATH environment variable. 20 | # 21 | # NOTE: This script MUST be sourced to update the environment of the calling 22 | # script. 23 | # 24 | # Usage instructions: 25 | # 26 | # source ./kokoro/testutils/install_go.sh 27 | 28 | set -eo pipefail 29 | 30 | readonly GO_VERSION="1.24.0" 31 | readonly GO_LINUX_AMD64_SHA256="dea9ca38a0b852a74e81c26134671af7c0fbe65d81b0dc1c5bfe22cf7d4c8858" 32 | readonly GO_DARWIN_AMD64_SHA256="7af054e5088b68c24b3d6e135e5ca8d91bbd5a05cb7f7f0187367b3e6e9e05ee" 33 | readonly PLATFORM="$(uname | tr '[:upper:]' '[:lower:]')" 34 | 35 | install_temp_go() { 36 | local go_platform 37 | local go_sha256 38 | case "${PLATFORM}" in 39 | "linux") 40 | go_platform="linux-amd64" 41 | go_sha256="${GO_LINUX_AMD64_SHA256}" 42 | ;; 43 | "darwin") 44 | go_platform="darwin-amd64" 45 | go_sha256="${GO_DARWIN_AMD64_SHA256}" 46 | ;; 47 | *) 48 | echo "Unsupported platform, unable to install Go." 49 | exit 1 50 | ;; 51 | esac 52 | readonly go_platform 53 | readonly go_sha256 54 | 55 | local -r go_archive="go${GO_VERSION}.${go_platform}.tar.gz" 56 | local -r go_url="https://go.dev/dl/${go_archive}" 57 | local -r go_tmpdir=$(mktemp -dt tink-go.XXXXXX) 58 | ( 59 | set -x 60 | cd "${go_tmpdir}" 61 | curl -OLsS "${go_url}" 62 | echo "${go_sha256} ${go_archive}" | sha256sum -c 63 | tar -xzf "${go_archive}" 64 | ) 65 | 66 | export GOROOT="${go_tmpdir}/go" 67 | export PATH="${go_tmpdir}/go/bin:${PATH}" 68 | } 69 | 70 | if [[ -n "${KOKORO_ARTIFACTS_DIR:-}" ]] ; then 71 | install_temp_go 72 | fi 73 | -------------------------------------------------------------------------------- /kokoro/testutils/test_utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ################################################################################ 16 | 17 | # Set of utilities to run unit tests for bash scripts. 18 | # 19 | # Example usage: 20 | # From your test script: 21 | # source some/path/to/test_utils.sh 22 | # 23 | # # Test functions must be defined as follows: 24 | # test__() { 25 | # # Do some ground work. 26 | # # Run the test script. 27 | # ./path/to/script_to_test ... 28 | # ASSERT_CMD_SUCCEEDED 29 | # ASSERT_FILE_EQUALS 30 | # } 31 | # 32 | # # Finds all the functions starting with `test_`, extracts test name and 33 | # # test case, and run them. 34 | # run_all_tests "$@" 35 | # 36 | 37 | # This is either set by Bazel or generated. 38 | : "${TEST_TMPDIR:="$(mktemp -td test.XXXXX)"}" 39 | readonly TEST_TMPDIR 40 | 41 | # Temporary directory for the testcase to use. 42 | TEST_CASE_TMPDIR= 43 | 44 | # Current test name. 45 | _CURRENT_TEST_SCOPE= 46 | 47 | # Current test case. 48 | _CURRENT_TEST_CASE= 49 | 50 | # True if at least one of the test cases terminated with an error. 51 | _HAS_ERROR="false" 52 | 53 | _print_testcase_failed_and_exit() { 54 | echo "[ FAILED ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}" 55 | exit 1 56 | } 57 | 58 | ####################################### 59 | # Starts a new test case. 60 | # 61 | # Globals: 62 | # _CURRENT_TEST_SCOPE 63 | # _CURRENT_TEST_CASE 64 | ####################################### 65 | _start_test_case() { 66 | echo "[ RUN ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}" 67 | # Create a tmp dir for the test case. 68 | TEST_CASE_TMPDIR="${TEST_TMPDIR}/${_CURRENT_TEST_SCOPE}/${_CURRENT_TEST_CASE}" 69 | mkdir -p "${TEST_CASE_TMPDIR}" 70 | } 71 | 72 | ####################################### 73 | # Ends a test case printing a success message. 74 | # 75 | # Globals: 76 | # _CURRENT_TEST_SCOPE 77 | # _CURRENT_TEST_CASE 78 | ####################################### 79 | _end_test_case_with_success() { 80 | test_case="$1" 81 | echo "[ OK ] ${_CURRENT_TEST_SCOPE}.${_CURRENT_TEST_CASE}" 82 | } 83 | 84 | ####################################### 85 | # Returns the list of tests defined in the test script. 86 | # 87 | # A test case is a function of the form: 88 | # test__ 89 | # 90 | # This function returns all the functions starting with `test_`. 91 | # 92 | # Globals: 93 | # None 94 | # Arguments: 95 | # None 96 | ####################################### 97 | _get_all_tests() { 98 | declare -F | 99 | while read line; do 100 | case "${line}" in "declare -f test_"*) 101 | echo "${line#declare -f }" 102 | ;; 103 | esac 104 | done 105 | } 106 | 107 | ####################################### 108 | # Runs a given test function. 109 | # 110 | # A test case is a function of the form: 111 | # test__ 112 | # 113 | # This script extracts test name and test case from the name. 114 | # 115 | # Globals: 116 | # _CURRENT_TEST_SCOPE 117 | # Arguments: 118 | # None 119 | ####################################### 120 | _do_run_test() { 121 | test_function="$1" 122 | IFS=_ read _CURRENT_TEST_SCOPE _CURRENT_TEST_CASE <<< "${test_function#test_}" 123 | _start_test_case 124 | ( 125 | # Make sure we exit only when assertions fail. 126 | set +e 127 | "${test_function}" 128 | ) 129 | local -r result=$? 130 | if (( $result == 0 )); then 131 | _end_test_case_with_success 132 | else 133 | _HAS_ERROR="true" 134 | fi 135 | } 136 | 137 | ####################################### 138 | # Runs all the test cases defined in the test script file. 139 | # Globals: 140 | # None 141 | # Arguments: 142 | # None 143 | # 144 | ####################################### 145 | run_all_tests() { 146 | for test in $(_get_all_tests); do 147 | _do_run_test "${test}" 148 | done 149 | # Make sure we return an error code for the failing test 150 | if [[ "${_HAS_ERROR}" == "true" ]]; then 151 | exit 1 152 | fi 153 | } 154 | 155 | ASSERT_CMD_SUCCEEDED() { 156 | if (( $? != 0 )); then 157 | _print_testcase_failed_and_exit 158 | fi 159 | } 160 | 161 | ASSERT_CMD_FAILED() { 162 | if (( $? == 0 )); then 163 | _print_testcase_failed_and_exit 164 | fi 165 | } 166 | 167 | ASSERT_FILE_EQUALS() { 168 | input_file="$1" 169 | expected_file="$2" 170 | if ! diff "${input_file}" "${expected_file}"; then 171 | _print_testcase_failed_and_exit 172 | fi 173 | } 174 | -------------------------------------------------------------------------------- /testdata/gcp/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) 4 | 5 | exports_files(srcs = ["credential.json"]) 6 | 7 | filegroup( 8 | name = "credentials", 9 | testonly = 1, 10 | srcs = [ 11 | "credential.json", 12 | "key_name.txt", 13 | ], 14 | ) 15 | 16 | filegroup( 17 | name = "bad_credentials", 18 | testonly = 1, 19 | srcs = [ 20 | "credential_bad.json", 21 | "key_name_bad.txt", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /testdata/gcp/README.md: -------------------------------------------------------------------------------- 1 | This folder contains GCP credentials that are used for testing Tink. 2 | 3 | For security reasons, all credentials in this folder are invalid. 4 | 5 | If you want to run tests that depend on them, please create your own 6 | [Cloud KMS key](https://cloud.google.com/kms/docs/creating-keys), and copy the 7 | credentials to `gcp/credential.json` and the key URI to `gcp/key_name.txt`. 8 | -------------------------------------------------------------------------------- /testdata/gcp/credential.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "tink-test-infrastructure", 4 | "private_key_id": "some_bad_private_key_id", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMtJlaQD79xGIC28\nowTpj7wkdi34piSubtDKttgC3lL00ioyQf/WMqLnyDWySNufCjhavQ7/sxXQAUCL\n5B3WDwM8+mFqQM2wJB18NBWBSfGOFSMwVQyWv7Y/1AFr+PvNKVlw4RZ4G8VuJzXZ\n9v/+5zyKv8py66sGVoHPI+LGfIprAgMBAAECgYEAxcgX8PVrnrITiKwpJxReJbyL\nxnpOmw2i/zza3BseVzOebjNrhw/NQDWl0qhcvmBjvyR5IGiiwiwXq8bu8CBdhRiE\nw3vKf1iuVOKhH07RB2wvCaGbVlB/p15gYau3sTRn5nej0tjYHX7xa/St/DwPk2H/\nxYGTRhyYtNL6wdtMjYECQQD+LVVJf0rLnxyPADTcz7Wdb+FUX79nWtMlzQOEB09+\nJj4ie0kD0cIvTQFjV3pOsg3uW2khFpjg110TXpJJfPjhAkEAzL7RhhfDdL7Dn2zl\n1orUthcGa2pzEAmg1tGBNb1pOg7LbVHKSa3GOOwyPRsActoyrPw18/fXaJdEfByY\ne9kwywJAB7rHMjH9y01uZ+bgtKpYYo5JcvBqeLEpZKfkaHp0b2ioURIguU4Csr+L\nwEKjxIrjo5ECFHCEe6nw+arRlgyH4QJBAIfQmEn733LEzB0n7npXU2yKb363eSYN\nTPzSsoREZdXWVIjqtWYUeKXvwA+apryJEw5+qwdvwxslJI+zpE6bLusCQE6M1lO9\nN6A3PtQv7Z3XwrEE/sPEVv4M4VHj0YHLs/32UuSXq5taMizKILfis1Stry4WjRHp\nQxEqdLrIkb13NH8=\n-----END PRIVATE KEY-----", 6 | "client_email": "unit-and-integration-testing@tink-test-infrastructure.iam.gserviceaccount.com", 7 | "client_id": "111876397550362269561", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/unit-and-integration-testing%40tink-test-infrastructure.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /testdata/gcp/credential_bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "tink-test-infrastructure", 4 | "private_key_id": "some_bad_private_key_id", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMtJlaQD79xGIC28\nowTpj7wkdi34piSubtDKttgC3lL00ioyQf/WMqLnyDWySNufCjhavQ7/sxXQAUCL\n5B3WDwM8+mFqQM2wJB18NBWBSfGOFSMwVQyWv7Y/1AFr+PvNKVlw4RZ4G8VuJzXZ\n9v/+5zyKv8py66sGVoHPI+LGfIprAgMBAAECgYEAxcgX8PVrnrITiKwpJxReJbyL\nxnpOmw2i/zza3BseVzOebjNrhw/NQDWl0qhcvmBjvyR5IGiiwiwXq8bu8CBdhRiE\nw3vKf1iuVOKhH07RB2wvCaGbVlB/p15gYau3sTRn5nej0tjYHX7xa/St/DwPk2H/\nxYGTRhyYtNL6wdtMjYECQQD+LVVJf0rLnxyPADTcz7Wdb+FUX79nWtMlzQOEB09+\nJj4ie0kD0cIvTQFjV3pOsg3uW2khFpjg110TXpJJfPjhAkEAzL7RhhfDdL7Dn2zl\n1orUthcGa2pzEAmg1tGBNb1pOg7LbVHKSa3GOOwyPRsActoyrPw18/fXaJdEfByY\ne9kwywJAB7rHMjH9y01uZ+bgtKpYYo5JcvBqeLEpZKfkaHp0b2ioURIguU4Csr+L\nwEKjxIrjo5ECFHCEe6nw+arRlgyH4QJBAIfQmEn733LEzB0n7npXU2yKb363eSYN\nTPzSsoREZdXWVIjqtWYUeKXvwA+apryJEw5+qwdvwxslJI+zpE6bLusCQE6M1lO9\nN6A3PtQv7Z3XwrEE/sPEVv4M4VHj0YHLs/32UuSXq5taMizKILfis1Stry4WjRHp\nQxEqdLrIkb13NH8=\n-----END PRIVATE KEY-----", 6 | "client_email": "unit-and-integration-testing@tink-test-infrastructure.iam.gserviceaccount.com", 7 | "client_id": "111876397550362269561", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/unit-and-integration-testing%40tink-test-infrastructure.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /testdata/gcp/key_name.txt: -------------------------------------------------------------------------------- 1 | projects/tink-test-infrastructure/locations/global/keyRings/unit-and-integration-testing/cryptoKeys/aead-key 2 | 3 | -------------------------------------------------------------------------------- /testdata/gcp/key_name_bad.txt: -------------------------------------------------------------------------------- 1 | projects/non-existing-project/locations/global/keyRings/some-key-ring/cryptoKeys/aead-key 2 | 3 | --------------------------------------------------------------------------------