├── .envrc ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ └── go-build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── config └── make.env ├── digests-dist.txt ├── digests.txt ├── dist ├── Formula │ └── turnkey.rb ├── release.env ├── turnkey.darwin-aarch64 ├── turnkey.darwin-x86_64 ├── turnkey.linux-aarch64 └── turnkey.linux-x86_64 ├── flake.lock ├── flake.nix ├── go.work ├── keys ├── 66039AA59D823C8BD68DB062D3EC673DF9843E7B.asc ├── 6B61ECD76088748C70590D55E90A401336C8AAA9.asc ├── A8864A8303994E3A18ACD1760CAB4418C834B102.asc └── DE050A451E6FAF94C677B58B9361DEC647A087BD.asc └── src ├── Dockerfile ├── brew └── formula.rb ├── cmd └── turnkey │ ├── fixtures │ ├── testkey.private │ └── testkey.public │ ├── main.go │ ├── main_test.go │ └── pkg │ ├── activities.go │ ├── address-formats.go │ ├── auth.go │ ├── cmd.go │ ├── curves.go │ ├── decrypt.go │ ├── encrypt.go │ ├── ethereum.go │ ├── format.go │ ├── generate.go │ ├── organizations.go │ ├── private-keys.go │ ├── raw.go │ ├── request.go │ ├── root.go │ ├── transaction-types.go │ ├── version.go │ └── wallets.go ├── go.mod └── go.sum /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc=" 3 | fi 4 | use flake 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/turnkey.* filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary & Motivation (Problem vs. Solution) 2 | 3 | ## Release Steps 4 | See README for additional details. 5 | 6 | - [ ] Tag the release (once approved) 7 | - [ ] Attest (once merged) 8 | - [ ] Create release with changelog 9 | - [ ] Update Homebrew tap 10 | -------------------------------------------------------------------------------- /.github/workflows/go-build.yml: -------------------------------------------------------------------------------- 1 | name: go-build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 # v3.2.0 16 | with: 17 | go-version: 1.21 18 | 19 | - name: Test 20 | env: 21 | GOHOSTOS: linux 22 | GOHOSTARCH: amd64 23 | GOOS: linux 24 | GOARCH: amd64 25 | run: | 26 | cd src 27 | go build -o ../out/turnkey.linux-x86_64 ./cmd/turnkey/ 28 | go test -v ./cmd/turnkey/... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/internal/version/data/ 2 | go.work.sum 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Apple folder files 18 | .DS_Store 19 | 20 | # Local build folder 21 | build/ 22 | 23 | # Vscode-related configs 24 | .vscode 25 | .direnv 26 | 27 | # Toolchain 28 | /cache 29 | /out 30 | .* 31 | dist/build.log 32 | digests.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Turnkey 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= $(shell git describe --tag --always --dirty --match 'v[0-9]*' --abbrev=8) 2 | 3 | KEYS := \ 4 | 6B61ECD76088748C70590D55E90A401336C8AAA9 \ 5 | A8864A8303994E3A18ACD1760CAB4418C834B102 \ 6 | 66039AA59D823C8BD68DB062D3EC673DF9843E7B \ 7 | DE050A451E6FAF94C677B58B9361DEC647A087BD 8 | 9 | LOCAL_BUILD_DIR := build 10 | SRC_DIR := src 11 | KEY_DIR := fetch/keys 12 | OUT_DIR := out 13 | DIST_DIR := dist 14 | 15 | lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) 16 | altarch = $(subst x86_64,amd64,$(subst aarch64,arm64,$1)) 17 | normarch = $(subst arm64,aarch64,$(subst amd64,x86_64,$1)) 18 | HOST_ARCH := $(call normarch,$(call lc,$(shell uname -m))) 19 | HOST_ARCH_ALT := $(call altarch,$(HOST_ARCH)) 20 | HOST_OS := $(call lc,$(shell uname -s)) 21 | 22 | GIT_REF := $(shell git log -1 --format=%H) 23 | GIT_AUTHOR := $(shell git log -1 --format=%an) 24 | GIT_KEY := $(shell git log -1 --format=%GP) 25 | GIT_TIMESTAMP := $(shell git log -1 --format=%cd --date=iso) 26 | 27 | REGISTRY := local 28 | BUILDER := $(shell which docker) 29 | 30 | # Build package with chosen $(BUILDER) 31 | # Supported BUILDERs: docker 32 | # Usage: $(call build,core/$(NAME),$(VERSION),$(TARGET),$(EXTRA_ARGS)) 33 | # Notes: 34 | # - Packages are expected to use the following layer names in order: 35 | # - "fetch": [optional] obtain any artifacts from the internet. 36 | # - "build": [optional] do any required build work 37 | # - "package": [required] scratch layer exporting artifacts for distribution 38 | # - "test": [optional] define any tests 39 | # - Packages may prefix layer names with "text-" if more than one is desired 40 | # - VERSION will be set as a build-arg if defined, otherwise it is "latest" 41 | # - TARGET defaults to "package" 42 | # - EXTRA_ARGS will be blindly injected 43 | # - packages may also define a "test" layer 44 | # - the ulimit line is to workaround a bug in patch when the nofile limit is too large: 45 | # https://savannah.gnu.org/bugs/index.php?62958 46 | # TODO: 47 | # - try to disable networking on fetch layers with something like: 48 | # $(if $(filter fetch,$(lastword $(subst -, ,$(TARGET)))),,--network=none) 49 | # - actually output OCI files for each build (vs plain tar) 50 | # - output manifest.txt of all tar/digest hashes for an easy git diff 51 | # - support buildah and podman 52 | define build 53 | $(eval $(call determine_platform,$(1))) 54 | echo PLATFORM is $(PLATFORM) 55 | echo HOST_ARCH_ALT is $(HOST_ARCH_ALT) 56 | echo HOST_OS is $(HOST_OS) 57 | $(eval LANGUAGE := go) 58 | $(eval NAME := $(2)) 59 | $(eval VERSION := $(if $(3),$(3),latest)) 60 | $(eval TARGET := $(if $(4),$(4),package)) 61 | $(eval EXTRA_ARGS := $(if $(5),$(5),)) 62 | $(eval BUILD_CMD := \ 63 | DOCKER_BUILDKIT=1 \ 64 | SOURCE_DATE_EPOCH=1 \ 65 | $(BUILDER) \ 66 | build \ 67 | --ulimit nofile=2048:16384 \ 68 | -t $(REGISTRY)/$(NAME):$(VERSION) \ 69 | --build-arg REGISTRY=$(REGISTRY) \ 70 | --build-arg LABEL=$(NAME) \ 71 | --build-arg ARCH=$(ARCH) \ 72 | --build-arg HOST_ARCH=$(HOST_ARCH) \ 73 | --build-arg GOARCH=$(HOST_ARCH_ALT) \ 74 | --build-arg GOOS=$(HOST_OS) \ 75 | --platform $(PLATFORM) \ 76 | $(if $(DOCKER_CACHE_SRC),$(DOCKER_CACHE_SRC),) \ 77 | $(if $(DOCKER_CACHE_DST),$(DOCKER_CACHE_DST),) \ 78 | --network=host \ 79 | --progress=plain \ 80 | $(if $(filter latest,$(VERSION)),,--build-arg VERSION=$(VERSION)) \ 81 | --target $(NAME) \ 82 | -f $(SRC_DIR)/Dockerfile \ 83 | $(EXTRA_ARGS) \ 84 | . \ 85 | ) 86 | $(eval TIMESTAMP := $(shell TZ=GMT date +"%Y-%m-%dT%H:%M:%SZ")) 87 | mkdir -p out/ 88 | echo $(TIMESTAMP) $(BUILD_CMD) >> out/build.log 89 | $(BUILD_CMD) 90 | endef 91 | 92 | define determine_platform 93 | ifeq ($1,linux-x86_64) 94 | PLATFORM := linux/amd64 95 | HOST_ARCH_ALT := amd64 96 | HOST_OS := linux 97 | else ifeq ($1,linux-aarch64) 98 | PLATFORM := linux/arm64 99 | HOST_ARCH_ALT := arm64 100 | HOST_OS := linux 101 | else ifeq ($1,darwin-x86_64) 102 | PLATFORM := linux/amd64 103 | HOST_ARCH_ALT := amd64 104 | HOST_OS := darwin 105 | else ifeq ($1,darwin-aarch64) 106 | PLATFORM := linux/arm64 107 | HOST_ARCH_ALT := arm64 108 | HOST_OS := darwin 109 | endif 110 | endef 111 | 112 | define go-build 113 | $(call build,$(3),$(2),latest) 114 | # $(if $(filter package,$(TARGET)),$(BUILDER) save $(REGISTRY)/$(NAME):$(VERSION) -o $@.docker.tar,) 115 | # Ignore errors from the docker rm; this is just to ensure no such container exists before we create it. 116 | docker rm -f $(2) 2> /dev/null 117 | docker create --name=$(2) local/$(2):$(VERSION) 118 | docker export $(2) -o $(OUT_DIR)/$(2).tar 119 | tar xf $(OUT_DIR)/$(2).tar -C $(OUT_DIR) app 120 | rm $(OUT_DIR)/$(2).tar 121 | mv $(OUT_DIR)/app $(OUT_DIR)/$(2) 122 | endef 123 | 124 | $(OUT_DIR)/turnkey.%: 125 | $(call go-build,cmd/turnkey,turnkey,$*) 126 | mv $(OUT_DIR)/turnkey $@ 127 | 128 | .DEFAULT_GOAL := 129 | .PHONY: default 130 | default: \ 131 | $(DEFAULT_GOAL) \ 132 | $(patsubst %,$(KEY_DIR)/%.asc,$(KEYS)) \ 133 | $(OUT_DIR)/turnkey.linux-x86_64 \ 134 | $(OUT_DIR)/turnkey.linux-aarch64 \ 135 | $(OUT_DIR)/turnkey.darwin-x86_64 \ 136 | $(OUT_DIR)/turnkey.darwin-aarch64 \ 137 | $(OUT_DIR)/Formula/turnkey.rb \ 138 | $(OUT_DIR)/release.env \ 139 | digests.txt 140 | 141 | .PHONY: digests.txt 142 | digests.txt: 143 | echo "Building digests.txt"; \ 144 | cd $(OUT_DIR); \ 145 | sha256sum turnkey.* > ../digests.txt 146 | 147 | .PHONY: lint 148 | lint: 149 | echo "Running lint"; \ 150 | cd $(SRC_DIR); \ 151 | golangci-lint run ./cmd/turnkey/... --timeout=3m || exit 1; 152 | 153 | .PHONY: test 154 | test: $(OUT_DIR)/turnkey.$(HOST_OS)-$(HOST_ARCH) 155 | echo "Running tests..."; \ 156 | cd $(SRC_DIR); \ 157 | go test -v ./cmd/turnkey/... 158 | 159 | .PHONY: install 160 | install: default 161 | mkdir -p ~/.local/bin 162 | cp $(OUT_DIR)/turnkey.$(HOST_OS)-$(HOST_ARCH) ~/.local/bin/turnkey 163 | 164 | .PHONY: clean 165 | clean: 166 | rm -rf $(LOCAL_BUILD_DIR) 167 | rm -rf $(OUT_DIR) 168 | 169 | $(KEY_DIR)/%.asc: 170 | $(call fetch_pgp_key,$(basename $(notdir $@))) 171 | 172 | $(OUT_DIR)/Formula/turnkey.rb: \ 173 | $(OUT_DIR)/turnkey.darwin-x86_64 \ 174 | $(OUT_DIR)/turnkey.darwin-aarch64 175 | mkdir -p $(OUT_DIR)/Formula 176 | export \ 177 | VERSION="$(VERSION)" \ 178 | DARWIN_X86_64_SHA256="$(shell \ 179 | openssl sha256 -r $(OUT_DIR)/turnkey.darwin-x86_64 \ 180 | | sed -e 's/ \*out\// /g' -e 's/ \.\// /g' -e 's/ .*//g' \ 181 | )" \ 182 | DARWIN_AARCH64_SHA256="$(shell \ 183 | openssl sha256 -r $(OUT_DIR)/turnkey.darwin-aarch64 \ 184 | | sed -e 's/ \*out\// /g' -e 's/ \.\// /g' -e 's/ .*//g' \ 185 | )"; \ 186 | cat $(SRC_DIR)/brew/formula.rb | envsubst > $@ 187 | 188 | $(OUT_DIR)/release.env: | $(OUT_DIR) 189 | echo 'VERSION=$(VERSION)' > $(OUT_DIR)/release.env 190 | echo 'GIT_REF=$(GIT_REF)' >> $(OUT_DIR)/release.env 191 | echo 'GIT_AUTHOR=$(GIT_AUTHOR)' >> $(OUT_DIR)/release.env 192 | echo 'GIT_KEY=$(GIT_KEY)' >> $(OUT_DIR)/release.env 193 | echo 'GIT_TIMESTAMP=$(GIT_TIMESTAMP)' >> $(OUT_DIR)/release.env 194 | 195 | .PHONY: build-local 196 | build-local: 197 | go build -o ./$(LOCAL_BUILD_DIR)/turnkey ./src/cmd/turnkey 198 | 199 | .PHONY: reproduce 200 | reproduce: clean default digests.txt 201 | @diff digests.txt digests-dist.txt \ 202 | && echo "Digests are identical" \ 203 | || echo "Warning: digests.txt and digests-dist.txt differ" 204 | 205 | .PHONY: $(DIST_DIR) 206 | $(DIST_DIR): clean default 207 | rm -rf $@/* 208 | cp digests.txt digests-dist.txt 209 | cp -R $(OUT_DIR)/* $@/ 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turnkey CLI 2 | 3 | [![Go Build Status](https://github.com/tkhq/tkcli/actions/workflows/go-build.yml/badge.svg)](https://github.com/tkhq/tkcli/actions/workflows/go-build.yml) 4 | 5 | ## Installation 6 | 7 | We have multiple ways to install the CLI depending on your threat model. 8 | 9 | Please check our work to whatever extent appropriate for your use case. 10 | 11 | ### Prerequisites 12 | 13 | The Makefile assumes the presence of a few basic tools: 14 | 15 | - `make` 16 | - `bash` 17 | - `Docker` 18 | 19 | ### Blind Trust 20 | 21 | > :warning: Before you copy/paste, note that these are /low/ security options 22 | 23 | If you are on an untrusted machine and are only evaluating our tools, we offer 24 | easy low security install paths common in the industry. 25 | 26 | Do note that any time you run an unverified binary off the internet you are 27 | giving a third party full permission to execute any code they want on your 28 | system. Github accounts, CDNs, and package repository accounts get compromised 29 | all the time. 30 | 31 | #### Download 32 | 33 | | Version | OS | Architecture | Download | 34 | | ------- | ----- | ------------ | ---------------------------------------------------------------------------------------------- | 35 | | v1.1.5 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.linux-x86_64) | 36 | | v1.1.5 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.linux-aarch64) | 37 | | v1.1.5 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.darwin-x86_64) | 38 | | v1.1.5 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.darwin-aarch64) | 39 | | v1.1.4 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.4/dist/turnkey.linux-x86_64) | 40 | | v1.1.4 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.4/dist/turnkey.linux-aarch64) | 41 | | v1.1.4 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.4/dist/turnkey.darwin-x86_64) | 42 | | v1.1.4 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.4/dist/turnkey.darwin-aarch64) | 43 | | v1.1.3 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.3/dist/turnkey.linux-x86_64) | 44 | | v1.1.3 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.3/dist/turnkey.linux-aarch64) | 45 | | v1.1.3 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.3/dist/turnkey.darwin-x86_64) | 46 | | v1.1.3 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.3/dist/turnkey.darwin-aarch64) | 47 | | v1.1.2 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.2/dist/turnkey.linux-x86_64) | 48 | | v1.1.2 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.2/dist/turnkey.linux-aarch64) | 49 | | v1.1.2 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.2/dist/turnkey.darwin-x86_64) | 50 | | v1.1.2 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.2/dist/turnkey.darwin-aarch64) | 51 | | v1.1.1 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.1/dist/turnkey.linux-x86_64) | 52 | | v1.1.1 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.1/dist/turnkey.linux-aarch64) | 53 | | v1.1.1 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.1.1/dist/turnkey.darwin-x86_64) | 54 | | v1.1.1 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.1.1/dist/turnkey.darwin-aarch64) | 55 | | v1.0.5 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.5/dist/turnkey.linux-x86_64) | 56 | | v1.0.5 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.5/dist/turnkey.linux-aarch64) | 57 | | v1.0.5 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.5/dist/turnkey.darwin-x86_64) | 58 | | v1.0.5 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.5/dist/turnkey.darwin-aarch64) | 59 | | v1.0.4 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.4/dist/turnkey.linux-x86_64) | 60 | | v1.0.4 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.4/dist/turnkey.linux-aarch64) | 61 | | v1.0.4 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.4/dist/turnkey.darwin-x86_64) | 62 | | v1.0.4 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.4/dist/turnkey.darwin-aarch64) | 63 | | v1.0.3 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.3/dist/turnkey.linux-x86_64) | 64 | | v1.0.3 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.3/dist/turnkey.linux-aarch64) | 65 | | v1.0.3 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.3/dist/turnkey.darwin-x86_64) | 66 | | v1.0.3 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.3/dist/turnkey.darwin-aarch64) | 67 | | v1.0.2 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.2/dist/turnkey.linux-x86_64) | 68 | | v1.0.2 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.2/dist/turnkey.linux-aarch64) | 69 | | v1.0.2 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.2/dist/turnkey.darwin-x86_64) | 70 | | v1.0.2 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.2/dist/turnkey.darwin-aarch64) | 71 | | v1.0.1 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.1/dist/turnkey.linux-x86_64) | 72 | | v1.0.1 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.1/dist/turnkey.linux-aarch64) | 73 | | v1.0.1 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.1/dist/turnkey.darwin-x86_64) | 74 | | v1.0.1 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.1/dist/turnkey.darwin-aarch64) | 75 | | v1.0.0 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.0/dist/turnkey.linux-x86_64) | 76 | | v1.0.0 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.0/dist/turnkey.linux-aarch64) | 77 | | v1.0.0 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v1.0.0/dist/turnkey.darwin-x86_64) | 78 | | v1.0.0 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v1.0.0/dist/turnkey.darwin-aarch64) | 79 | | v0.3.4 | Linux | x86_64 | [turnkey.linux-x86_64](https://github.com/tkhq/tkcli/raw/v0.3.4/dist/turnkey.linux-x86_64) | 80 | | v0.3.4 | Linux | aarch64 | [turnkey.linux-aarch64](https://github.com/tkhq/tkcli/raw/v0.3.4/dist/turnkey.linux-aarch64) | 81 | | v0.3.4 | MacOS | x86_64 | [turnkey.darwin-x86_64](https://github.com/tkhq/tkcli/raw/v0.3.4/dist/turnkey.darwin-x86_64) | 82 | | v0.3.4 | MacOS | aarch64 | [turnkey.darwin-aarch64](https://github.com/tkhq/tkcli/raw/v0.3.4/dist/turnkey.darwin-aarch64) | 83 | 84 | #### Git 85 | 86 | ```sh 87 | git clone https://github.com/thkq/tkcli 88 | cd tkcli 89 | # This installs in ~/.local/bin; make sure this is in your $PATH! 90 | make install 91 | ``` 92 | 93 | #### Brew 94 | 95 | ```sh 96 | brew install tkhq/tap/turnkey 97 | ``` 98 | 99 | ### Moderate Trust 100 | 101 | These steps will allow you to prove that at least two Turnkey engineers 102 | signed off on the produced binaries, signaling that they reproduced them from 103 | source code and got identical results, in addition to our usual two-party code 104 | review processes. 105 | 106 | This minimizes a single point of trust (and failure) in our binary release 107 | process. 108 | 109 | See the [Reproducible Builds](https://reproducible-builds.org/) project for 110 | more information on these practices. 111 | 112 | We use git for all development, releases, and signing. Unfortunately git has no 113 | native method for large file storage or multi-signature workflows so some git 114 | add-ons are required. 115 | 116 | To follow these steps please install [git-lfs][gl] and [git-sig][gs]. 117 | 118 | [gs]: https://codeberg.org/distrust/git-sig 119 | [gl]: https://git-lfs.com 120 | 121 | 1. Clone repo 122 | 123 | ```sh 124 | git clone https://github.com/tkhq/tkcli 125 | cd tkcli 126 | ``` 127 | 128 | 2. Review binary signatures 129 | 130 | ```sh 131 | git sig verify 132 | ``` 133 | 134 | Note: See Trust section below for expected keys/signers 135 | 136 | 3. Install binary 137 | 138 | ``` 139 | make install 140 | ``` 141 | 142 | ### Zero Trust 143 | 144 | If you intend to use the Turnkey CLI on a system you need to be able to trust 145 | or for a high risk use case, we strongly recommend taking the time to hold us 146 | accountable to the maximum degree you have resources and time for. 147 | 148 | This protects not only you, but also protects our team. If many people are 149 | checking our work for tampering it removes the incentive for someone malicious 150 | to attempt to force one or more of us to tamper with the software. 151 | 152 | 1. Clone repo 153 | 154 | ```sh 155 | git clone https://github.com/tkhq/tkcli 156 | cd tkcli 157 | ``` 158 | 159 | 2. Review source 160 | 161 | - Ideal: Review the entire supply chain is recommended for high risk uses 162 | - Minimal: review the "attest" "sign" and "verify" targets in the Makefile 163 | 164 | 3. Reproduce binaries 165 | 166 | ```sh 167 | make reproduce 168 | ``` 169 | 170 | Note: See Trust section below for expected keys/signers 171 | 172 | 4. Install binaries 173 | 174 | ```sh 175 | make install 176 | ``` 177 | 178 | 5. Upload signature 179 | 180 | While this step is totally optional, if you took the time to verify our 181 | binaries we would welcome you signing them and submitting your signature so 182 | we have public evidence third parties are checking our work. 183 | 184 | **NOTE**: this additionally uses Github's official CLI tool, [gh](https://github.com/cli/cli). 185 | 186 | ```sh 187 | gh repo fork 188 | git add dist/* 189 | git commit -m "add signature" 190 | git sig add 191 | git push origin main 192 | gh pr create 193 | ``` 194 | 195 | ## Usage 196 | 197 | Create a new API key: 198 | 199 | ```sh 200 | $ turnkey generate api-key --organization $ORGANIZATION_ID 201 | { 202 | "privateKeyFile": "/Users/andrew/Library/Application Support/turnkey/keys/default.private", 203 | "publicKey": "0236f17892a4649d97b2e4a4ad3c22d815e4e77848a0b8e4a5b0956ae4d6be382e", 204 | "publicKeyFile": "/Users/andrew/Library/Application Support/turnkey/keys/default.public" 205 | } 206 | ``` 207 | 208 | Make an API request (using the default API key created above): 209 | 210 | ```sh 211 | $ turnkey request --path /api/v1/sign --body '{"payload": "hello from TKHQ"}' 212 | { 213 | "result": "I am a teapot" 214 | } 215 | ``` 216 | 217 | If you need to sign a request with a different key, use the `--key-name` and/or `--keys-folder` flags: 218 | 219 | ```sh 220 | $ turnkey request --path /api/v1/sign --body '{"payload": "hello from TKHQ"}' --keys-folder /path/to/keys --key-name another-key 221 | { 222 | "result": "I am a teapot" 223 | } 224 | ``` 225 | 226 | Create, but do not _post_ a request: 227 | 228 | ```sh 229 | $ turnkey request --no-post --path /api/v1/sign --body '{"payload": "hello from TKHQ"}' 230 | { 231 | "curlCommand": "curl -X POST -d'{\"payload\": \"hello from TKHQ\"}' -H'X-Stamp: eyJwdWJsaWNLZXkiOiIwM2JmMTYyNTc2ZWI4ZGZlY2YzM2Q5Mjc1ZDA5NTk1Mjg0ZjZjNGRmMGRiNjE1NmMzYzU4Mjc3Nzg4NmEwZWUwYWMiLCJzaWduYXR1cmUiOiIzMDQ0MDIyMDZiMmRlYmIwYjA3YmYwMDJlMjI1ZmQ4NTgzZjZmNGUxNGE5YTUxYWRiYWJjNDAyYzY5YTZlN2Q4N2ViNWNjMDgwMjIwMjE0ZTdkMGJlODFjMGYyNDEyOWE0MmNkZGFlOTUxYTBmZTViMGM1Mzc3YjM2NzZiOTUyNDgyNmYwODdhMWU4ZiIsInNjaGVtZSI6IlNJR05BVFVSRV9TQ0hFTUVfVEtfQVBJX1AyNTYifQ' -v 'https://coordinator-beta.turnkey.io/api/v1/sign'", 232 | "message": "{\"payload\": \"hello from TKHQ\"}", 233 | "stamp": "eyJwdWJsaWNLZXkiOiIwM2JmMTYyNTc2ZWI4ZGZlY2YzM2Q5Mjc1ZDA5NTk1Mjg0ZjZjNGRmMGRiNjE1NmMzYzU4Mjc3Nzg4NmEwZWUwYWMiLCJzaWduYXR1cmUiOiIzMDQ0MDIyMDZiMmRlYmIwYjA3YmYwMDJlMjI1ZmQ4NTgzZjZmNGUxNGE5YTUxYWRiYWJjNDAyYzY5YTZlN2Q4N2ViNWNjMDgwMjIwMjE0ZTdkMGJlODFjMGYyNDEyOWE0MmNkZGFlOTUxYTBmZTViMGM1Mzc3YjM2NzZiOTUyNDgyNmYwODdhMWU4ZiIsInNjaGVtZSI6IlNJR05BVFVSRV9TQ0hFTUVfVEtfQVBJX1AyNTYifQ" 234 | } 235 | ``` 236 | 237 | ## Building 238 | 239 | ### Build for all platforms 240 | 241 | ```sh 242 | make 243 | ``` 244 | 245 | ### Build for one platform 246 | 247 | ```sh 248 | make out/turnkey.linux-amd64 249 | ``` 250 | 251 | ### Local build (for development only) 252 | 253 | The following will drop a binary in `build/turnkey`: 254 | 255 | ```sh 256 | make build-local 257 | ``` 258 | 259 | Note that you may need to do the following: 260 | 261 | - Install `git-lfs`: https://git-lfs.com 262 | - Setup: `git lfs install` 263 | 264 | ## Release 265 | 266 | To release a new version of the CLI: 267 | 268 | Determine the next version: 269 | 270 | ```sh 271 | git tag | sort -n | tail -n5 272 | ``` 273 | 274 | Export your new version: 275 | 276 | ```sh 277 | export VERSION=vX.Y.Z 278 | ``` 279 | 280 | Build the release artifacts: 281 | 282 | ```sh 283 | make VERSION=$VERSION dist 284 | ``` 285 | 286 | Cut a new release branch: 287 | 288 | ```sh 289 | git checkout -b release-$VERSION 290 | ``` 291 | 292 | Open a pull request, and once you have enough approvals, tag the release: 293 | 294 | ```sh 295 | git tag -sa $VERSION -m "New release: $VERSION" 296 | ``` 297 | 298 | Finally, update the download table above, with links pointing to the new binaries. 299 | 300 | Once the pull request is merged, ask your reviewer(s) to attest with `git sig`: 301 | 302 | ```sh 303 | make reproduce 304 | 305 | # If the reproduce command succeeds: 306 | git sig add 307 | ``` 308 | 309 | Once enough signatures have been collected, the following command should succeed: 310 | 311 | ```sh 312 | git sig verify --threshold 2 313 | ``` 314 | 315 | Finally, post the new release on Github with a changelog and update the Homebrew tap. 316 | 317 | ## Trust 318 | 319 | ### Process 320 | 321 | You should never trust random binaries or code you find on the internet. Even 322 | if it is from a reputable git identity, developers are phished all the time. 323 | 324 | Supply chain attacks are becoming increasingly common in our industry and it 325 | takes strong accountability to prevent them from happening. 326 | 327 | The only way to be reasonably confident code was actually authored by the 328 | people we think it was, is if that software is cryptographically signed by a 329 | key only those individuals have access to. 330 | 331 | Similarly if a company releases binaries, you have no idea if the machine that 332 | compiled it is compromised or not, and no idea if the code in that binary 333 | corresponds to the actual code in the repo that you or someone you trust 334 | authored or reviewed. 335 | 336 | To address both problems we take the following steps: 337 | 338 | 1. All commits are signed with keys that only exist on hardware security 339 | modules held by each engineer 340 | 2. All binaries are signed by the engineer that compiled them 341 | 3. Attesting engineers compile and sign binaries if they get the same hashes 342 | 343 | ### Signature Verification 344 | 345 | To learn who signed the current release run: 346 | 347 | `git sig verify --threshold 2` 348 | 349 | Commits will be signed by at least one of the keys under the signers section 350 | below. 351 | 352 | Released binaries should be signed by at least two of them signifying 353 | successful reproducible builds. 354 | 355 | We encourage you to review the below keyoxide links and any available 356 | web-of-trust for each key to ensure it is really owned by the person it claims 357 | to be owned by. 358 | 359 | ### Signers 360 | 361 | | Name | PGP Fingerprint | 362 | | ---------------- | ------------------------------------------------------------------------------------------ | 363 | | Andrew Min | [DE05 0A45 1E6F AF94 C677 B58B 9361 DEC6 47A0 87BD](https://keyoxide.org/9361DEC647A087BD) | 364 | | Arnaud Brousseau | [6870 5ACF 41E8 ECDE E292 5A42 4AAB 800C FFA3 065A](https://keyoxide.org/4AAB800CFFA3065A) | 365 | | Keyan Zhang | [0211 6F38 FB32 9E98 65A1 D08B 5880 CFD7 A7D9 5342](https://keyoxide.org/5880CFD7A7D95342) | 366 | | Lance Vick | [6B61 ECD7 6088 748C 7059 0D55 E90A 4013 36C8 AAA9](https://keyoxide.org/E90A401336C8AAA9) | 367 | | Seán C McCord | [39B2 095B 61DD 23EE E1BF 883A 8A1F 0484 90D2 3AFD](https://keyoxide.org/8A1F048490D23AFD) | 368 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for taking the time to contribute to the security of Turnkey! We appreciate your effort in identifying and reporting vulnerabilities. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you discover a security vulnerability, refer to https://docs.turnkey.com/security/reporting-a-vulnerability to disclose it to us confidentially. 8 | 9 | We encourage PGP-encrypted email submission if possible. 10 | -------------------------------------------------------------------------------- /config/make.env: -------------------------------------------------------------------------------- 1 | DEBIAN_HASH=7b1f092d471519e5c2063824436cb5d64dda9f2dc78d94dfbb9e0e21a801ea97 2 | GOCACHE=/home/build/cache 3 | GOPATH=/home/build/cache 4 | CGO_ENABLED=0 5 | GOHOSTOS=linux 6 | GOHOSTARCH=amd64 7 | GO_URL=https://go.dev/dl/go1.21.0.src.tar.gz 8 | GO_HASH=818d46ede85682dd551ad378ef37a4d247006f12ec59b5b755601d2ce114369a 9 | BUSYBOX_URL=https://busybox.net/downloads/busybox-1.36.1.tar.bz2 10 | BUSYBOX_HASH=b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314 -------------------------------------------------------------------------------- /digests-dist.txt: -------------------------------------------------------------------------------- 1 | 729804cf6652e23b8e3fc0a2548e0e0327b69826f50b10b560656f77af76f575 turnkey.darwin-aarch64 2 | 36ddc3f9675214c35e924f8212028e35d7674ae1e6a46a49d68fa4b983c1d954 turnkey.darwin-x86_64 3 | 20f87614b1763314c04cb2539d26b2a8aea396f3dcd3bb879aa7bca214fdb777 turnkey.linux-aarch64 4 | 9eb4d7f96870f42ad01f67f2a948b8e57dac0ed838163e5e35bb1194b0b978bf turnkey.linux-x86_64 5 | -------------------------------------------------------------------------------- /digests.txt: -------------------------------------------------------------------------------- 1 | 729804cf6652e23b8e3fc0a2548e0e0327b69826f50b10b560656f77af76f575 turnkey.darwin-aarch64 2 | 36ddc3f9675214c35e924f8212028e35d7674ae1e6a46a49d68fa4b983c1d954 turnkey.darwin-x86_64 3 | 20f87614b1763314c04cb2539d26b2a8aea396f3dcd3bb879aa7bca214fdb777 turnkey.linux-aarch64 4 | 9eb4d7f96870f42ad01f67f2a948b8e57dac0ed838163e5e35bb1194b0b978bf turnkey.linux-x86_64 5 | -------------------------------------------------------------------------------- /dist/Formula/turnkey.rb: -------------------------------------------------------------------------------- 1 | class Turnkey < Formula 2 | desc "Turnkey CLI" 3 | homepage "https://github.com/tkhq/tkcli" 4 | version "v1.1.5" 5 | license "Apache License 2.0" 6 | 7 | if Hardware::CPU.arm? 8 | url "https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.darwin-aarch64", using: CurlDownloadStrategy 9 | sha256 "729804cf6652e23b8e3fc0a2548e0e0327b69826f50b10b560656f77af76f575" 10 | 11 | def install 12 | bin.install "turnkey.darwin-aarch64" => "turnkey" 13 | end 14 | end 15 | if Hardware::CPU.intel? 16 | url "https://github.com/tkhq/tkcli/raw/v1.1.5/dist/turnkey.darwin-x86_64", using: CurlDownloadStrategy 17 | sha256 "36ddc3f9675214c35e924f8212028e35d7674ae1e6a46a49d68fa4b983c1d954" 18 | 19 | def install 20 | bin.install "turnkey.darwin-x86_64" => "turnkey" 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /dist/release.env: -------------------------------------------------------------------------------- 1 | VERSION=v1.1.5 2 | GIT_REF=4ca5c0022c48debfdf5ad8a619265262f4498e54 3 | GIT_AUTHOR=t-vila 4 | GIT_KEY= 5 | GIT_TIMESTAMP=2025-05-29 21:34:57 +0300 6 | -------------------------------------------------------------------------------- /dist/turnkey.darwin-aarch64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkhq/tkcli/418a12911ef76f732788613828dca13a14e258b6/dist/turnkey.darwin-aarch64 -------------------------------------------------------------------------------- /dist/turnkey.darwin-x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkhq/tkcli/418a12911ef76f732788613828dca13a14e258b6/dist/turnkey.darwin-x86_64 -------------------------------------------------------------------------------- /dist/turnkey.linux-aarch64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkhq/tkcli/418a12911ef76f732788613828dca13a14e258b6/dist/turnkey.linux-aarch64 -------------------------------------------------------------------------------- /dist/turnkey.linux-x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkhq/tkcli/418a12911ef76f732788613828dca13a14e258b6/dist/turnkey.linux-x86_64 -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1697226376, 24 | "narHash": "sha256-cumLLb1QOUtWieUnLGqo+ylNt3+fU8Lcv5Zl+tYbRUE=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "898cb2064b6e98b8c5499f37e81adbdf2925f7c5", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-23.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "tkcli devshell"; 3 | inputs = { 4 | nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05"; 5 | #nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # localstack is broken right now (2023-01-25) in unstable, due to a missing dependency 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils, ... }@inputs: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | 14 | gci = pkgs.buildGoModule rec { 15 | name = "gci"; 16 | src = pkgs.fetchFromGitHub { 17 | owner = "daixiang0"; 18 | repo = "gci"; 19 | rev = "v0.10.1"; 20 | sha256 = "sha256-/YR61lovuYw+GEeXIgvyPbesz2epmQVmSLWjWwKT4Ag="; 21 | }; 22 | 23 | # Switch to fake vendor sha for upgrades: 24 | #vendorSha256 = pkgs.lib.fakeSha256; 25 | vendorSha256 = "sha256-g7htGfU6C2rzfu8hAn6SGr0ZRwB8ZzSf9CgHYmdupE8="; 26 | }; 27 | 28 | tkbuild = pkgs.writeScriptBin "build" '' 29 | #!/bin/sh 30 | pushd $(git rev-parse --show-toplevel)/src 31 | ${pkgs.go}/bin/go build -o $(go env GOPATH)/bin/turnkey 32 | ${pkgs.go}/bin/go build -o ../out/turnkey.linux-x86_64 # hack for local CLI go test 33 | ''; 34 | 35 | tklint = pkgs.writeScriptBin "lint" '' 36 | #!/bin/sh 37 | pushd $(git rev-parse --show-toplevel)/src 38 | ${pkgs.go}/bin/go mod tidy 39 | ${pkgs.gofumpt}/bin/gofumpt -w *.go ./cmd/* 40 | ${gci}/bin/gci write --skip-generated -s standard -s default -s "Prefix(github.com/tkhq)" . 41 | ${pkgs.golangci-lint}/bin/golangci-lint run ./... 42 | ${pkgs.go}/bin/go build -o ../out/turnkey.linux-x86_64 # hack for local CLI go test 43 | ${pkgs.go}/bin/go test -v ./... 44 | ''; 45 | in 46 | { 47 | devShells.default = pkgs.mkShell { 48 | packages = with pkgs; [ 49 | bashInteractive 50 | envsubst 51 | gci 52 | gofumpt 53 | golangci-lint 54 | go 55 | go-swagger 56 | go-tools 57 | tkbuild 58 | tklint 59 | ]; 60 | }; 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.21 2 | 3 | toolchain go1.21.0 4 | 5 | use ./src 6 | -------------------------------------------------------------------------------- /keys/66039AA59D823C8BD68DB062D3EC673DF9843E7B.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGJqp+YBEAC6GaDNLNxVmWZk5RQ+AZFpoEupSOHBqgZFe3bNmZ+xfQ/l7moz 4 | Bdx9iVlZe+FlDtADBYWga3s/yxod3XbpODerh6LTLnvI5nr7jPy2MMqcKwkiUBRa 5 | Uxm2owTtmg8glEx3XLivFKqTnkzqFgZvx1BTNuzrLNPvAsYpvY0+njPNBktoocP2 6 | BJIChblI+TJBz906uhPJmu1ZlCpaOtLb+WjDV2Hwbolx9NJdpJzEEybplrn+V508 7 | DENimfmlraTU/u1SSY5H7pTkBCZH4P02od+elOZ38+hmFc8nFfFPvfbanTRSe0b4 8 | AQRICZNWqjxAomsZu3fZVdY/DDI89OcaW1jL8Ng3ja7o/K3Z2n2vZk1j9inPeCkn 9 | n8TzbknEDXkwsr0YrbqfguXG8ySiGgV/FpIsuVWka8hWChVY0b+qWrRCWk7qgvTJ 10 | JRd75sk5KPDYqgUHQ/8kVaula4mUTqZMZnuiug/SmNaih1oGv7yjwvcHI8RjnFMe 11 | xgM9lKYo5J2lb38sBXojlQ3qs1s16DjoJRnodNVKlJUN29QzQomJLHGYpawLR+2R 12 | +WuguoV1Heiqb393bjnCk6Bhg7kI0CyyUpOkrPtk3xSxA3QOdeMWngf8XQgfJo8k 13 | aYV7FOyXTQDSvgDnbbffIw7lcokP5BMdX7gkpNaJfN1hJBboQCkml/uHLwARAQAB 14 | tCFBcm5hdWQgQnJvdXNzZWF1IDxybm9AdHVybmtleS5pbz6JAlEEEwEIADsWIQRo 15 | cFrPQejs3uKSWkJKq4AM/6MGWgUCYmqn5gIbAQULCQgHAgIiAgYVCgkICwIEFgID 16 | AQIeBwIXgAAKCRBKq4AM/6MGWhHvD/4sLt6L9OTTs+BME8igQht7YGX9cpbuUcq7 17 | stx/0iBP+ik8AYSDI3JyEDGrcIk5jA4kXeAmsNuhu8DOLH2M3b6AFP2PMCVAfDbj 18 | oDbGjA8F9SVrirzk7vO3Q4ruTY+03qlsj2Xl53mOISm1c0YDPH4t7X0rFJ0EpJC8 19 | nJDravo4FrXckMBdfsO29l/f8WGRNMtDf0Zza8rPl3znIlJsnDoxWPjqijlZ4Wju 20 | Cr5VriQ4Hn28btQxHm0gf5pyKwbReFB8NUak4YCyMF1X9RaWubbjBEbFXXyUzAVc 21 | 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 107 | PmDGj1czdpaGbxpa//DwF44lxW1s+WVo 108 | =n76j 109 | -----END PGP PUBLIC KEY BLOCK----- 110 | -------------------------------------------------------------------------------- /keys/A8864A8303994E3A18ACD1760CAB4418C834B102.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 107 | +ppM8u0U/wy8lcEBb47cBiFj6lHq 108 | =NwiA 109 | -----END PGP PUBLIC KEY BLOCK----- 110 | -------------------------------------------------------------------------------- /keys/DE050A451E6FAF94C677B58B9361DEC647A087BD.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 107 | r9YJOHsaNnp56h6K9q/lT42g3Ses 108 | =ybfT 109 | -----END PGP PUBLIC KEY BLOCK----- 110 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM stagex/busybox:latest@sha256:a925ae77d4c8e633bc1706e23543a4b1b3a2b183257ed840b2c77aaaadeb3465 AS busybox 2 | FROM stagex/ca-certificates:latest@sha256:c6f95ed1ae7ec9e6e19d24fc4569bf46ae886042ebc4a16cc8ec53b762290379 AS ca-certificates 3 | FROM stagex/go:v1.21.4@sha256:26c3aab06c302fe6397793cc18608993834cdaadd96c059d214a96929af9baaf AS go 4 | 5 | FROM scratch AS base 6 | LABEL org.opencontainers.image.source https://github.com/tkhq/tkcli 7 | COPY --from=ca-certificates . / 8 | ENTRYPOINT ["/app"] 9 | 10 | FROM base AS fetch 11 | ENV GOPROXY="https://proxy.golang.org,direct" 12 | COPY --from=busybox . / 13 | COPY --from=go . / 14 | 15 | FROM fetch AS build 16 | RUN go env -w GOMODCACHE=/go/pkg/mod 17 | RUN go env -w GOSUMDB=sum.golang.org 18 | ARG BUILD_SRC_DIR 19 | ARG GO_BUILDFLAGS="-trimpath -buildvcs=false" 20 | ARG GO_LDFLAGS="-s -w -buildid= -extldflags=-static" 21 | ARG GOOS 22 | ARG GOARCH 23 | 24 | COPY . /home/user/tkcli 25 | 26 | # Prefetch go modules 27 | FROM build AS fetch_modules 28 | WORKDIR /home/user/tkcli 29 | RUN --mount=type=cache,target=/go/pkg/mod go mod download -json 30 | 31 | # Turnkey 32 | FROM build AS turnkey_build 33 | WORKDIR /home/user/tkcli/src/cmd/turnkey 34 | RUN go env GOMODCACHE 35 | RUN --mount=type=cache,target=/go/pkg/mod go build \ 36 | ${GO_BUILDFLAGS} \ 37 | -ldflags="${GO_LDFLAGS}" \ 38 | -o /home/user/bin/app . 39 | 40 | FROM base AS turnkey 41 | ARG LABEL 42 | LABEL org.opencontainers.image.title ${LABEL} 43 | COPY --from=turnkey_build /home/user/bin/app /app 44 | USER 100:100 -------------------------------------------------------------------------------- /src/brew/formula.rb: -------------------------------------------------------------------------------- 1 | class Turnkey < Formula 2 | desc "Turnkey CLI" 3 | homepage "https://github.com/tkhq/tkcli" 4 | version "$VERSION" 5 | license "Apache License 2.0" 6 | 7 | if Hardware::CPU.arm? 8 | url "https://github.com/tkhq/tkcli/raw/$VERSION/dist/turnkey.darwin-aarch64", using: CurlDownloadStrategy 9 | sha256 "$DARWIN_AARCH64_SHA256" 10 | 11 | def install 12 | bin.install "turnkey.darwin-aarch64" => "turnkey" 13 | end 14 | end 15 | if Hardware::CPU.intel? 16 | url "https://github.com/tkhq/tkcli/raw/$VERSION/dist/turnkey.darwin-x86_64", using: CurlDownloadStrategy 17 | sha256 "$DARWIN_X86_64_SHA256" 18 | 19 | def install 20 | bin.install "turnkey.darwin-x86_64" => "turnkey" 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /src/cmd/turnkey/fixtures/testkey.private: -------------------------------------------------------------------------------- 1 | 8e121c46cdf8c784e5209b882b1cd2e65712180a9b9201c1e7788ea7d4111541 -------------------------------------------------------------------------------- /src/cmd/turnkey/fixtures/testkey.public: -------------------------------------------------------------------------------- 1 | 0305acbc8b7751b7703736ae16cb22112451372f7b77717bbecdfa8300d4038432 -------------------------------------------------------------------------------- /src/cmd/turnkey/main.go: -------------------------------------------------------------------------------- 1 | // Package main provides the base executable harness for the Turnkey CLI. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/tkhq/tkcli/src/cmd/turnkey/pkg" 10 | ) 11 | 12 | func main() { 13 | if err := pkg.Execute(); err != nil { 14 | enc := json.NewEncoder(os.Stderr) 15 | enc.SetIndent("", " ") 16 | 17 | if encErr := enc.Encode(err); encErr != nil { 18 | fmt.Print(err) 19 | } 20 | 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/cmd/turnkey/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/google/uuid" 16 | "github.com/stretchr/testify/assert" 17 | 18 | "github.com/tkhq/go-sdk/pkg/apikey" 19 | ) 20 | 21 | var TurnkeyBinaryName = "turnkey.linux-x86_64" 22 | 23 | // TempDir is the directory in which temporary files for the tests will be stored. 24 | var TempDir = "/tmp" 25 | 26 | func init() { 27 | if os.Getenv("RUNNER_TEMP") != "" { 28 | TempDir = os.Getenv("RUNNER_TEMP") 29 | } 30 | var arch string 31 | switch runtime.GOARCH { 32 | case "arm64": 33 | arch = "aarch64" 34 | case "amd64": 35 | arch = "x86_64" 36 | } 37 | TurnkeyBinaryName = fmt.Sprintf("turnkey.%s-%s", runtime.GOOS, arch) 38 | } 39 | 40 | func RunCliWithArgs(t *testing.T, args []string) (string, error) { 41 | currentDir, err := os.Getwd() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | cmd := exec.Command(path.Join(currentDir, "../../../out/", TurnkeyBinaryName), args...) 47 | output, err := cmd.CombinedOutput() 48 | 49 | return string(output), err 50 | } 51 | 52 | func TestHelpText(t *testing.T) { 53 | out, err := RunCliWithArgs(t, []string{}) 54 | assert.Nil(t, err) 55 | assert.Contains(t, out, "the Turnkey CLI") 56 | assert.Contains(t, out, "Usage:") 57 | assert.Contains(t, out, "Available Commands:") 58 | } 59 | 60 | func TestAPIKeygenInTmpFolder(t *testing.T) { 61 | orgID := uuid.New() 62 | 63 | tmpDir, err := os.MkdirTemp(TempDir, "keys") 64 | assert.Nil(t, err) 65 | 66 | defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }() 67 | 68 | out, err := RunCliWithArgs(t, []string{"generate", "api-key", "--keys-folder", tmpDir, "--key-name", "mykey", "--organization", orgID.String()}) 69 | assert.Nil(t, err) 70 | 71 | assert.FileExists(t, tmpDir+"/mykey.public") 72 | assert.FileExists(t, tmpDir+"/mykey.private") 73 | 74 | publicKeyData, err := os.ReadFile(tmpDir + "/mykey.public") 75 | assert.Nil(t, err) 76 | 77 | var parsedOut map[string]string 78 | 79 | assert.Nil(t, json.Unmarshal([]byte(out), &parsedOut)) 80 | 81 | assert.Equal(t, parsedOut["publicKey"], string(publicKeyData)) 82 | assert.Equal(t, parsedOut["publicKeyFile"], tmpDir+"/mykey.public") 83 | assert.Equal(t, parsedOut["privateKeyFile"], tmpDir+"/mykey.private") 84 | } 85 | 86 | func TestEncryptionKeygenInTmpFolder(t *testing.T) { 87 | orgID := uuid.New() 88 | userID := uuid.New() 89 | 90 | tmpDir, err := os.MkdirTemp(TempDir, "encryption-keys") 91 | assert.Nil(t, err) 92 | 93 | defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }() 94 | 95 | out, err := RunCliWithArgs(t, []string{"generate", "encryption-key", "--encryption-keys-folder", tmpDir, "--encryption-key-name", "mykey", "--organization", orgID.String(), "--user", userID.String()}) 96 | assert.Nil(t, err) 97 | 98 | assert.FileExists(t, tmpDir+"/mykey.public") 99 | assert.FileExists(t, tmpDir+"/mykey.private") 100 | 101 | publicKeyData, err := os.ReadFile(tmpDir + "/mykey.public") 102 | assert.Nil(t, err) 103 | 104 | var parsedOut map[string]string 105 | 106 | assert.Nil(t, json.Unmarshal([]byte(out), &parsedOut)) 107 | 108 | assert.Equal(t, parsedOut["publicKey"], string(publicKeyData)) 109 | assert.Equal(t, parsedOut["publicKeyFile"], tmpDir+"/mykey.public") 110 | assert.Equal(t, parsedOut["privateKeyFile"], tmpDir+"/mykey.private") 111 | } 112 | 113 | func TestAPIKeygenDetectExistingKey(t *testing.T) { 114 | orgID := uuid.New() 115 | 116 | tmpDir, err := os.MkdirTemp(TempDir, "keys") 117 | defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }() 118 | 119 | assert.Nil(t, err) 120 | 121 | err = os.WriteFile(tmpDir+"/myexistingkey.public", []byte("mykey.public"), 0o755) 122 | assert.Nil(t, err) 123 | 124 | err = os.WriteFile(tmpDir+"/myexistingkey.private", []byte("mykey.private"), 0o755) 125 | assert.Nil(t, err) 126 | 127 | assert.FileExists(t, tmpDir+"/myexistingkey.public") 128 | assert.FileExists(t, tmpDir+"/myexistingkey.private") 129 | 130 | _, err = RunCliWithArgs(t, []string{"generate", "api-key", "--organization", orgID.String(), "--keys-folder", tmpDir, "--key-name", "myexistingkey"}) 131 | assert.NotNil(t, err) 132 | assert.Equal(t, err.Error(), "exit status 1") 133 | } 134 | 135 | func TestEncryptionKeygenDetectExistingKey(t *testing.T) { 136 | orgID := uuid.New() 137 | userID := uuid.New() 138 | 139 | tmpDir, err := os.MkdirTemp(TempDir, "encryption-keys") 140 | defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }() 141 | 142 | assert.Nil(t, err) 143 | 144 | err = os.WriteFile(tmpDir+"/myexistingkey.public", []byte("mykey.public"), 0o755) 145 | assert.Nil(t, err) 146 | 147 | err = os.WriteFile(tmpDir+"/myexistingkey.private", []byte("mykey.private"), 0o755) 148 | assert.Nil(t, err) 149 | 150 | assert.FileExists(t, tmpDir+"/myexistingkey.public") 151 | assert.FileExists(t, tmpDir+"/myexistingkey.private") 152 | 153 | _, err = RunCliWithArgs(t, []string{"generate", "encryption-key", "--organization", orgID.String(), "--user", userID.String(), "--encryption-keys-folder", tmpDir, "--encryption-key-name", "myexistingkey"}) 154 | assert.NotNil(t, err) 155 | assert.Equal(t, err.Error(), "exit status 1") 156 | } 157 | 158 | func TestStamp(t *testing.T) { 159 | orgID := uuid.New() 160 | 161 | out, err := RunCliWithArgs(t, []string{"request", "--no-post", "--keys-folder", ".", "--organization", orgID.String(), "--key-name", "fixtures/testkey.private", "--body", "hello!"}) 162 | assert.Nil(t, err) 163 | 164 | var parsedOut map[string]string 165 | 166 | assert.Nil(t, json.Unmarshal([]byte(out), &parsedOut)) 167 | 168 | stamp := parsedOut["stamp"] 169 | 170 | pubkeyBytes, err := os.ReadFile("fixtures/testkey.public") 171 | assert.Nil(t, err) 172 | 173 | ensureValidStamp(t, stamp, string(pubkeyBytes)) 174 | } 175 | 176 | func TestApproveRequest(t *testing.T) { 177 | orgID := uuid.New() 178 | 179 | out, err := RunCliWithArgs(t, []string{"request", "--no-post", "--keys-folder", ".", "--organization", orgID.String(), "--key-name", "fixtures/testkey.private", "--body", "{\"some\": \"field\"}", "--path", "/some/endpoint"}) 180 | assert.Nil(t, err) 181 | 182 | var parsedOut map[string]string 183 | err = json.Unmarshal([]byte(out), &parsedOut) 184 | assert.Nil(t, err) 185 | 186 | stamp := parsedOut["stamp"] 187 | pubkeyBytes, err := os.ReadFile("fixtures/testkey.public") 188 | assert.Nil(t, err) 189 | ensureValidStamp(t, stamp, string(pubkeyBytes)) 190 | 191 | assert.Equal(t, "{\"some\": \"field\"}", parsedOut["message"]) 192 | 193 | assert.Contains(t, parsedOut["curlCommand"], "curl -X POST -d'{\"some\": \"field\"}'") 194 | assert.Contains(t, parsedOut["curlCommand"], fmt.Sprintf("-H'X-Stamp: %s'", stamp)) 195 | assert.Contains(t, parsedOut["curlCommand"], "https://api.turnkey.com/some/endpoint") 196 | } 197 | 198 | func ensureValidStamp(t *testing.T, stamp string, expectedPublicKey string) { 199 | stampBytes, err := base64.RawURLEncoding.DecodeString(stamp) 200 | assert.Nil(t, err) 201 | 202 | var parsedStamp *apikey.APIStamp 203 | 204 | assert.Nil(t, json.Unmarshal(stampBytes, &parsedStamp)) 205 | 206 | assert.Equal(t, expectedPublicKey, parsedStamp.PublicKey) 207 | 208 | // All signatures start with 30.... 209 | assert.True(t, strings.HasPrefix(parsedStamp.Signature, "30")) 210 | 211 | _, err = hex.DecodeString(parsedStamp.Signature) 212 | 213 | // Ensure there is no issue decoding the signature as a hexadecimal string 214 | assert.Nil(t, err) 215 | } 216 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/activities.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/tkhq/go-sdk/pkg/api/client/activities" 7 | "github.com/tkhq/go-sdk/pkg/api/models" 8 | ) 9 | 10 | var activitiesListStatus []string 11 | 12 | func init() { 13 | activitiesListCmd.Flags().StringSliceVar(&activitiesListStatus, "status", nil, "only include activities whose status is declared in this set") 14 | 15 | rootCmd.AddCommand(activitiesCmd) 16 | 17 | activitiesCmd.AddCommand(activitiesListCmd) 18 | activitiesCmd.AddCommand(activitiesGetCmd) 19 | } 20 | 21 | var activitiesCmd = &cobra.Command{ 22 | Use: "activities", 23 | Short: "Interact with the API activities", 24 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 25 | basicSetup(cmd) 26 | LoadKeypair("") 27 | LoadClient() 28 | }, 29 | } 30 | 31 | var activitiesListCmd = &cobra.Command{ 32 | Use: "list", 33 | Short: "Return the set of activities for an organization", 34 | Run: func(cmd *cobra.Command, args []string) { 35 | activitiesFilter := make([]models.ActivityStatus, len(activitiesListStatus)) 36 | 37 | for i, s := range activitiesListStatus { 38 | if s == Help { 39 | Output(models.ActivityStatusEnum) 40 | 41 | return 42 | } 43 | 44 | if s == "all" { 45 | activitiesFilter = models.ActivityStatusEnum 46 | break 47 | } 48 | 49 | switch s { 50 | case "created": 51 | activitiesFilter[i] = models.ActivityStatusCreated 52 | case "pending": 53 | activitiesFilter[i] = models.ActivityStatusPending 54 | case "completed": 55 | activitiesFilter[i] = models.ActivityStatusCompleted 56 | case "failed": 57 | activitiesFilter[i] = models.ActivityStatusFailed 58 | case "consensus": 59 | activitiesFilter[i] = models.ActivityStatusConsensusNeeded 60 | case "consensus_needed": 61 | activitiesFilter[i] = models.ActivityStatusConsensusNeeded 62 | case "rejected": 63 | activitiesFilter[i] = models.ActivityStatusRejected 64 | default: 65 | activitiesFilter[i] = models.ActivityStatus(s) 66 | } 67 | } 68 | 69 | params := activities.NewGetActivitiesParams().WithDefaults().WithBody(&models.GetActivitiesRequest{ 70 | FilterByStatus: activitiesFilter, 71 | OrganizationID: &Organization, 72 | }) 73 | 74 | res, err := APIClient.V0().Activities.GetActivities(params, APIClient.Authenticator) 75 | if err != nil { 76 | OutputError(err) 77 | } 78 | 79 | Output(res.GetPayload().Activities) 80 | }, 81 | } 82 | 83 | var activitiesGetCmd = &cobra.Command{ 84 | Use: "get ", 85 | Short: "Return the details and status of a particular activity", 86 | Args: cobra.ExactArgs(1), 87 | Run: func(cmd *cobra.Command, args []string) { 88 | id := args[0] 89 | 90 | params := activities.NewGetActivityParams().WithDefaults().WithBody(&models.GetActivityRequest{ 91 | ActivityID: &id, 92 | OrganizationID: &Organization, 93 | }) 94 | 95 | res, err := APIClient.V0().Activities.GetActivity(params, APIClient.Authenticator) 96 | if err != nil { 97 | OutputError(err) 98 | } 99 | 100 | Output(res.GetPayload().Activity) 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/address-formats.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/tkhq/go-sdk/pkg/api/models" 7 | ) 8 | 9 | func init() { 10 | addressFormatsCmd.AddCommand(addressFormatsListCmd) 11 | 12 | rootCmd.AddCommand(addressFormatsCmd) 13 | } 14 | 15 | var addressFormatsCmd = &cobra.Command{ 16 | Use: "address-formats", 17 | Short: "Interact with the available address formats", 18 | } 19 | 20 | var addressFormatsListCmd = &cobra.Command{ 21 | Use: "list", 22 | Short: "Return the available key formats", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | Output(models.AddressFormatEnum) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/auth.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/rotisserie/eris" 7 | 8 | "github.com/tkhq/go-sdk" 9 | "github.com/tkhq/go-sdk/pkg/api/client" 10 | "github.com/tkhq/go-sdk/pkg/apikey" 11 | ) 12 | 13 | // APIKeypair is the loaded API Keypair. 14 | var APIKeypair *apikey.Key 15 | 16 | // APIClient is the API Client. 17 | var APIClient *sdk.Client 18 | 19 | // LoadKeypair require-loads the keypair referenced by the given name or as referenced form the global KeyName variable, if name is empty. 20 | func LoadKeypair(name string) { 21 | if name == "" { 22 | name = ApiKeyName 23 | } 24 | 25 | if apiKeyStore == nil { 26 | OutputError(eris.New("keystore not loaded")) 27 | } 28 | 29 | apiKey, err := apiKeyStore.Load(name) 30 | if err != nil { 31 | OutputError(err) 32 | } 33 | 34 | if apiKey == nil { 35 | OutputError(eris.New("API key not loaded")) 36 | } 37 | 38 | APIKeypair = apiKey 39 | 40 | // If we haven't had the organization explicitly set try to load it from key metadata. 41 | if Organization == "" { 42 | // Add the first (and for now only) org in the key metadata 43 | for _, o := range APIKeypair.Organizations { 44 | Organization = o 45 | 46 | break 47 | } 48 | } 49 | 50 | // If org is _still_ empty, the API key is not usable. 51 | if Organization == "" { 52 | OutputError(eris.New("failed to associate the API key with an organization; please manually specify the organization ID")) 53 | } 54 | } 55 | 56 | // LoadClient creates an API client from the preloaded API keypair. 57 | func LoadClient() { 58 | scheme := "https" 59 | if pattern := regexp.MustCompile(`^localhost:\d+$`); pattern.MatchString(apiHost) { 60 | scheme = "http" 61 | } 62 | transportConfig := client.DefaultTransportConfig().WithHost(apiHost).WithSchemes([]string{scheme}) 63 | 64 | APIClient = &sdk.Client{ 65 | Client: client.NewHTTPClientWithConfig(nil, transportConfig), 66 | Authenticator: &sdk.Authenticator{Key: APIKeypair}, 67 | APIKey: APIKeypair, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/cmd.go: -------------------------------------------------------------------------------- 1 | // Package cmd defines CLI modules 2 | package pkg 3 | 4 | // Help defines the help command for parameters offering enums. 5 | const Help = "help" 6 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/curves.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/tkhq/go-sdk/pkg/api/models" 7 | ) 8 | 9 | func init() { 10 | curvesCmd.AddCommand(curvesListCmd) 11 | 12 | rootCmd.AddCommand(curvesCmd) 13 | } 14 | 15 | var curvesCmd = &cobra.Command{ 16 | Use: "curves", 17 | Short: "Interact with the available curves", 18 | } 19 | 20 | var curvesListCmd = &cobra.Command{ 21 | Use: "list", 22 | Short: "Return the available curve types", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | Output(models.CurveEnum) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/decrypt.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/btcsuite/btcutil/base58" 9 | "github.com/rotisserie/eris" 10 | "github.com/spf13/cobra" 11 | "github.com/tkhq/go-sdk/pkg/enclave_encrypt" 12 | "github.com/tkhq/go-sdk/pkg/encryptionkey" 13 | ) 14 | 15 | var ( 16 | // Filepath to read the export bundle from. 17 | exportBundlePath string 18 | 19 | // EncryptionKeypair is the loaded Encryption Keypair. 20 | EncryptionKeypair *encryptionkey.Key 21 | 22 | // Solana address, required for exporting Solana private keys in the proper format 23 | solanaAddress string 24 | ) 25 | 26 | func init() { 27 | decryptCmd.Flags().StringVar(&exportBundlePath, "export-bundle-input", "", "filepath to read the export bundle from.") 28 | decryptCmd.Flags().StringVar(&plaintextPath, "plaintext-output", "", "optional filepath to write the plaintext from that will be decrypted.") 29 | decryptCmd.Flags().StringVar(&signerPublicKeyOverride, "signer-quorum-key", "", "optional override for the signer quorum key. This option should be used for testing only. Leave this value empty for production decryptions.") 30 | decryptCmd.Flags().StringVar(&solanaAddress, "solana-address", "", "optional solana address, for use when exporting solana private keys.") 31 | 32 | rootCmd.AddCommand(decryptCmd) 33 | } 34 | 35 | var decryptCmd = &cobra.Command{ 36 | Use: "decrypt", 37 | Short: "Decrypt a ciphertext", 38 | Long: `Decrypt a ciphertext from a bundle exported from a Turnkey secure enclave.`, 39 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 40 | basicSetup(cmd) 41 | LoadEncryptionKeypair("") 42 | }, 43 | PreRun: func(cmd *cobra.Command, args []string) { 44 | if exportBundlePath == "" { 45 | OutputError(eris.New("--export-bundle-input must be specified")) 46 | } 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | // read from export bundle path 50 | exportBundle, err := readFile(exportBundlePath) 51 | if err != nil { 52 | OutputError(err) 53 | } 54 | 55 | // get encryption key 56 | tkPrivateKey := EncryptionKeypair.GetPrivateKey() 57 | kemPrivateKey, err := encryptionkey.DecodeTurnkeyPrivateKey(tkPrivateKey) 58 | if err != nil { 59 | OutputError(eris.Wrap(err, "failed to decode encryption private key")) 60 | } 61 | 62 | var signerKey *ecdsa.PublicKey 63 | if signerPublicKeyOverride != "" { 64 | signerKey, err = hexToPublicKey(signerPublicKeyOverride) 65 | } else { 66 | signerKey, err = hexToPublicKey(signerProductionPublicKey) 67 | } 68 | if err != nil { 69 | OutputError(err) 70 | } 71 | 72 | // set up enclave encrypt client 73 | encryptClient, err := enclave_encrypt.NewEnclaveEncryptClientFromTargetKey(signerKey, *kemPrivateKey) 74 | if err != nil { 75 | OutputError(err) 76 | } 77 | 78 | // decrypt ciphertext 79 | plaintextBytes, err := encryptClient.Decrypt([]byte(exportBundle), Organization) 80 | if err != nil { 81 | OutputError(eris.Errorf("unable to decrypt export bundle: %v", err)) 82 | } 83 | 84 | plaintext := string(plaintextBytes) 85 | 86 | // apply formatting, if applicable 87 | if solanaAddress != "" { 88 | hexEncodedPlaintext := hex.EncodeToString(plaintextBytes) 89 | 90 | decodedAddressBytes := base58.Decode(solanaAddress) 91 | decodedAddress := hex.EncodeToString(decodedAddressBytes) 92 | 93 | combinedHex := fmt.Sprintf("%s%s", hexEncodedPlaintext, decodedAddress) 94 | combinedBytes, err := hex.DecodeString(combinedHex) 95 | if err != nil { 96 | OutputError(eris.Errorf("unable to decode combined hex string: %v", err)) 97 | } 98 | 99 | plaintext = base58.Encode(combinedBytes) 100 | } 101 | 102 | // output the plaintext if no filepath is passed 103 | if plaintextPath == "" { 104 | Output(plaintext) 105 | return 106 | } 107 | 108 | err = writeFile(plaintext, plaintextPath) 109 | if err != nil { 110 | OutputError(eris.Errorf("unable to write plaintext secret to file: %v", err)) 111 | } 112 | }, 113 | } 114 | 115 | // LoadEncryptionKeypair require-loads the keypair referenced by the given name or as referenced from the global EncryptionKeyName variable, if name is empty. 116 | func LoadEncryptionKeypair(name string) { 117 | if name == "" { 118 | name = EncryptionKeyName 119 | } 120 | 121 | if encryptionKeyStore == nil { 122 | OutputError(eris.New("encryption keystore not loaded")) 123 | } 124 | 125 | encryptionKey, err := encryptionKeyStore.Load(name) 126 | if err != nil { 127 | OutputError(eris.Wrap(err, "encryption key not found, run `turnkey generate encryption-key` to create one")) 128 | } 129 | 130 | if encryptionKey == nil { 131 | OutputError(eris.New("encryption key not loaded")) 132 | } 133 | 134 | EncryptionKeypair = encryptionKey 135 | 136 | // If we haven't had the organization explicitly set try to load it from key metadata. 137 | if Organization == "" { 138 | Organization = encryptionKey.Organization 139 | } 140 | 141 | // If org is _still_ empty, the encryption key is not usable. 142 | if Organization == "" { 143 | OutputError(eris.New("failed to associate the encryption key with an organization; please manually specify the organization ID")) 144 | } 145 | 146 | // If we haven't had the user explicitly set try to load it from key metadata. 147 | if User == "" { 148 | User = encryptionKey.User 149 | } 150 | 151 | // If user is _still_ empty, the encryption key is still usable in some cases where user ID isn't needed (export) 152 | // Hence we do not error out here if encryptionKey.User is empty. 153 | } 154 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/encrypt.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "encoding/hex" 6 | "encoding/json" 7 | 8 | "github.com/btcsuite/btcutil/base58" 9 | "github.com/rotisserie/eris" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/tkhq/go-sdk/pkg/enclave_encrypt" 13 | ) 14 | 15 | var ( 16 | // user is the user ID to import wallets and private keys with. 17 | User string 18 | 19 | // Filepath to write the import bundle to. 20 | importBundlePath string 21 | 22 | // Filepath to read the encrypted bundle from. 23 | encryptedBundlePath string 24 | 25 | // Filepath to read the plaintext from that will be encrypted. 26 | plaintextPath string 27 | 28 | // Format to apply to the plaintext key before it's encrypted: `mnemonic`, `hexadecimal`, `solana`. Defaults to `mnemonic`. 29 | keyFormat string 30 | 31 | // Signer quorum key in hex, uncompressed format 32 | signerPublicKeyOverride string 33 | ) 34 | 35 | func init() { 36 | encryptCmd.Flags().StringVar(&importBundlePath, "import-bundle-input", "", "filepath to read the import bundle from (result of init-import).") 37 | encryptCmd.Flags().StringVar(&encryptedBundlePath, "encrypted-bundle-output", "", "filepath to write the encrypted bundle to. This encrypted bundle will be part of the final import activity params (--encrypted-bundle-input option in wallet or private key import commands).") 38 | encryptCmd.Flags().StringVar(&plaintextPath, "plaintext-input", "", "filepath to read the plaintext from that will be encrypted.") 39 | encryptCmd.Flags().StringVar(&keyFormat, "key-format", "mnemonic", "optional formatting to apply to the plaintext before it is encrypted.") 40 | encryptCmd.Flags().StringVar(&User, "user", "", "ID of user to encrypting the plaintext.") 41 | encryptCmd.Flags().StringVar(&signerPublicKeyOverride, "signer-quorum-key", "", "optional override for the signer quorum key. This option should be used for testing only. Leave this value empty for production encryptions.") 42 | 43 | rootCmd.AddCommand(encryptCmd) 44 | } 45 | 46 | var encryptCmd = &cobra.Command{ 47 | Use: "encrypt", 48 | Short: "Encrypt a plaintext", 49 | Long: `Encrypt a plaintext into a bundle to be imported to a Turnkey secure enclave.`, 50 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 51 | basicSetup(cmd) 52 | LoadEncryptionKeypair("") 53 | }, 54 | PreRun: func(cmd *cobra.Command, args []string) { 55 | if importBundlePath == "" { 56 | OutputError(eris.New("--import-bundle-input must be specified")) 57 | } 58 | 59 | if encryptedBundlePath == "" { 60 | OutputError(eris.New("--encrypted-bundle-output must be specified")) 61 | } 62 | 63 | if plaintextPath == "" { 64 | OutputError(eris.New("--plaintext-input must be specified")) 65 | } 66 | }, 67 | Run: func(cmd *cobra.Command, args []string) { 68 | // read from import bundle path 69 | importBundle, err := readFile(importBundlePath) 70 | if err != nil { 71 | OutputError(err) 72 | } 73 | 74 | // set up enclave encrypt client 75 | var signerKey *ecdsa.PublicKey 76 | if signerPublicKeyOverride != "" { 77 | signerKey, err = hexToPublicKey(signerPublicKeyOverride) 78 | } else { 79 | signerKey, err = hexToPublicKey(signerProductionPublicKey) 80 | } 81 | if err != nil { 82 | OutputError(err) 83 | } 84 | 85 | encryptClient, err := enclave_encrypt.NewEnclaveEncryptClient(signerKey) 86 | if err != nil { 87 | OutputError(err) 88 | } 89 | 90 | // format the plaintext key 91 | plaintext, err := readFile(plaintextPath) 92 | if err != nil { 93 | OutputError(err) 94 | } 95 | var plaintextBytes []byte 96 | switch keyFormat { 97 | case "mnemonic": 98 | plaintextBytes = []byte(plaintext) 99 | case "hexadecimal": 100 | plaintextBytes, err = hex.DecodeString(plaintext) 101 | if err != nil { 102 | OutputError(err) 103 | } 104 | case "solana": 105 | decoded := base58.Decode(plaintext) 106 | if len(decoded) < 32 { 107 | OutputError(eris.New("invalid plaintext length. must be at least 32 bytes for key-format `solana`")) 108 | } 109 | plaintextBytes = decoded[:32] 110 | default: 111 | OutputError(eris.New("--key-format is invalid. accepted values: mnemonic, hexadecimal, solana.")) 112 | } 113 | 114 | // encrypt plaintext 115 | clientSendMsg, err := encryptClient.Encrypt(plaintextBytes, []byte(importBundle), Organization, User) 116 | if err != nil { 117 | OutputError(err) 118 | } 119 | 120 | encryptedBundleBytes, err := json.Marshal(clientSendMsg) 121 | if err != nil { 122 | OutputError(err) 123 | } 124 | 125 | // write to encrypted bundle path 126 | err = writeFile(string(encryptedBundleBytes), encryptedBundlePath) 127 | if err != nil { 128 | OutputError(err) 129 | } 130 | }, 131 | } 132 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/ethereum.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/rotisserie/eris" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/tkhq/go-sdk/pkg/api/client/signing" 8 | "github.com/tkhq/go-sdk/pkg/api/models" 9 | "github.com/tkhq/go-sdk/pkg/util" 10 | ) 11 | 12 | var ( 13 | ethTxSigner string 14 | ethTxPayload string 15 | ) 16 | 17 | func init() { 18 | rootCmd.AddCommand(ethCmd) 19 | 20 | ethTxCmd.Flags().StringVarP(ðTxSigner, "signer", "s", "", "wallet account address, private key address, or private key ID") 21 | ethTxCmd.Flags().StringVar(ðTxPayload, "payload", "", "payload of the transaction") 22 | 23 | ethCmd.AddCommand(ethTxCmd) 24 | } 25 | 26 | var ethCmd = &cobra.Command{ 27 | Use: "ethereum", 28 | Short: "Perform actions related to Ethereum", 29 | Aliases: []string{"eth"}, 30 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 31 | basicSetup(cmd) 32 | LoadKeypair("") 33 | LoadClient() 34 | }, 35 | } 36 | 37 | var ethTxCmd = &cobra.Command{ 38 | Use: "transaction", 39 | Short: "Perform signing and other actions for a transaction", 40 | Aliases: []string{"tx"}, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | transactionType := models.TransactionTypeEthereum 43 | activityType := string(models.ActivityTypeSignTransaction) 44 | 45 | payload, err := ParameterToString(ethTxPayload) 46 | if err != nil { 47 | OutputError(eris.Wrap(err, "failed to read payload")) 48 | } 49 | 50 | // NB: eventually, we should add ways of creating transaction payloads, to be more helpful. 51 | // Until then, this is an error. 52 | if payload == "" { 53 | OutputError(eris.New("payload cannot be empty")) 54 | } 55 | 56 | params := signing.NewSignTransactionParams().WithBody( 57 | &models.SignTransactionRequest{ 58 | OrganizationID: &Organization, 59 | Parameters: &models.SignTransactionIntentV2{ 60 | SignWith: ðTxSigner, 61 | Type: &transactionType, 62 | UnsignedTransaction: &payload, 63 | }, 64 | TimestampMs: util.RequestTimestamp(), 65 | Type: &activityType, 66 | }, 67 | ) 68 | 69 | resp, err := APIClient.V0().Signing.SignTransaction(params, APIClient.Authenticator) 70 | if err != nil { 71 | OutputError(eris.Wrap(err, "request failed")) 72 | } 73 | 74 | if !resp.IsSuccess() { 75 | OutputError(eris.Errorf("failed to create private key: %s", resp.Error())) 76 | } 77 | 78 | Output(resp.Payload) 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/format.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | var outputFormat string 12 | 13 | type encoder interface { 14 | Encode(data any) error 15 | } 16 | 17 | func init() { 18 | rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "json", "output format") 19 | } 20 | 21 | func getEncoder() encoder { 22 | switch outputFormat { 23 | case "yaml": 24 | enc := yaml.NewEncoder(os.Stdout) 25 | enc.SetIndent(2) 26 | 27 | return enc 28 | default: // JSON is the default 29 | enc := json.NewEncoder(os.Stdout) 30 | enc.SetIndent("", " ") 31 | 32 | return enc 33 | } 34 | } 35 | 36 | // OutputError prints an error to the console and exits. 37 | func OutputError(err error) { 38 | if err = getEncoder().Encode(map[string]string{ 39 | "error": err.Error(), 40 | }); err != nil { 41 | fmt.Fprintf(os.Stderr, "failed to write error to output encoder: %s", err) 42 | } 43 | 44 | os.Exit(1) 45 | } 46 | 47 | // Output prints to the console and exits. 48 | func Output(payload any) { 49 | payload = maybeParseJSON(payload) 50 | if err := getEncoder().Encode(payload); err != nil { 51 | fmt.Fprintf(os.Stderr, "failed to encode output: %s", err) 52 | } 53 | 54 | os.Exit(0) 55 | } 56 | 57 | // ResponseError is a structured format to display an HTTP error response. 58 | type ResponseError struct { 59 | Code int `json:"responseCode"` 60 | Text string `json:"responseBody"` 61 | } 62 | 63 | func (r *ResponseError) Error() string { 64 | return fmt.Sprintf("%d: %s", r.Code, r.Text) 65 | } 66 | 67 | // In case the payload is already JSON-encoded, try decoding it before passing it on. 68 | // Otherwise it leads to double-encoding. 69 | // This is the case for e.g. HTTP response bytes (they're generally JSON-encoded strings). 70 | // If the payload isn't a valid byte array, or not a valid JSON-encoded string, it is returned as-is. 71 | func maybeParseJSON(payload any) any { 72 | bytes, ok := payload.([]byte) 73 | if ok { 74 | var decoded any 75 | err := json.Unmarshal(bytes, &decoded) 76 | if err == nil { 77 | return decoded 78 | } 79 | 80 | } 81 | return payload 82 | } 83 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/generate.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rotisserie/eris" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/tkhq/go-sdk/pkg/apikey" 10 | "github.com/tkhq/go-sdk/pkg/encryptionkey" 11 | "github.com/tkhq/go-sdk/pkg/store/local" 12 | ) 13 | 14 | var ( 15 | curveType string 16 | ) 17 | 18 | func init() { 19 | apiKeyCmd.Flags().StringVar(&curveType, "curve", "p256", "curve type for API key; p256, secp256k1, and ed25519 currently supported") 20 | generateCmd.AddCommand(apiKeyCmd) 21 | 22 | encryptionKeyCmd.Flags().StringVar(&User, "user", "", "ID of user to generating the encryption key") 23 | generateCmd.AddCommand(encryptionKeyCmd) 24 | 25 | rootCmd.AddCommand(generateCmd) 26 | } 27 | 28 | // generateCmd represents the base command for generating different kinds of keys 29 | var generateCmd = &cobra.Command{ 30 | Use: "generate", 31 | Short: "Generate keys", 32 | } 33 | 34 | // Represents the command to generate an API key 35 | var apiKeyCmd = &cobra.Command{ 36 | Use: "api-key", 37 | Short: "Generate a Turnkey API key", 38 | Long: `Generate a new API key that can be used for authenticating with the API.`, 39 | PreRun: func(cmd *cobra.Command, args []string) { 40 | if Organization == "" { 41 | OutputError(eris.New("--organization must be specified")) 42 | } 43 | }, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | name, err := cmd.Flags().GetString("key-name") 46 | if err != nil { 47 | OutputError(eris.Wrap(err, "failed to read API key name")) 48 | } 49 | 50 | curveType, err := cmd.Flags().GetString("curve") 51 | if err != nil { 52 | OutputError(eris.Wrap(err, "failed to read curve type")) 53 | } 54 | 55 | var apiKey *apikey.Key 56 | 57 | switch curveType { 58 | default: 59 | OutputError(fmt.Errorf("invalid curve type: %s; supported types are p256, secp256k1, and ed25519", curveType)) 60 | case string(apikey.CurveP256): 61 | apiKey, err = apikey.New(Organization, apikey.WithScheme(apikey.SchemeP256)) 62 | if err != nil { 63 | OutputError(eris.Wrap(err, "failed to create API keypair")) 64 | } 65 | case string(apikey.CurveSecp256k1): 66 | apiKey, err = apikey.New(Organization, apikey.WithScheme(apikey.SchemeSECP256K1)) 67 | if err != nil { 68 | OutputError(eris.Wrap(err, "failed to create API keypair")) 69 | } 70 | case string(apikey.CurveEd25519): 71 | apiKey, err = apikey.New(Organization, apikey.WithScheme(apikey.SchemeED25519)) 72 | if err != nil { 73 | OutputError(eris.Wrap(err, "failed to create API keypair")) 74 | } 75 | } 76 | 77 | if name == "-" { 78 | Output(map[string]string{ 79 | "publicKey": apiKey.TkPublicKey, 80 | "privateKey": apiKey.TkPrivateKey, 81 | }) 82 | } 83 | 84 | apiKey.Metadata.Name = name 85 | apiKey.Metadata.PublicKey = apiKey.TkPublicKey 86 | apiKey.Metadata.Scheme = apiKey.Scheme 87 | apiKey.Metadata.Organizations = []string{Organization} 88 | 89 | if err = apiKeyStore.Store(name, apiKey); err != nil { 90 | OutputError(eris.Wrap(err, "failed to store new API keypair")) 91 | } 92 | 93 | localStore, ok := apiKeyStore.(*local.Store[*apikey.Key, apikey.Metadata]) 94 | if !ok { 95 | OutputError(eris.Wrap(err, "unhandled keystore type: expected *local.Store")) 96 | } 97 | 98 | Output(map[string]string{ 99 | "publicKey": apiKey.TkPublicKey, 100 | "publicKeyFile": localStore.PublicKeyFile(name), 101 | "privateKeyFile": localStore.PrivateKeyFile(name), 102 | }) 103 | }, 104 | } 105 | 106 | // Represents the command to generate an encryption key 107 | var encryptionKeyCmd = &cobra.Command{ 108 | Use: "encryption-key", 109 | Short: "Generate a Turnkey encryption key", 110 | Long: `Generate a new encryption key that can be used for encrypting text sent from Turnkey secure enclaves.`, 111 | PreRun: func(cmd *cobra.Command, args []string) { 112 | if Organization == "" { 113 | OutputError(eris.New("--organization must be specified")) 114 | } 115 | 116 | if User == "" { 117 | OutputError(eris.New("--user must be specified")) 118 | } 119 | }, 120 | Run: func(cmd *cobra.Command, args []string) { 121 | name, err := cmd.Flags().GetString("encryption-key-name") 122 | if err != nil { 123 | OutputError(eris.Wrap(err, "failed to read encryption key name")) 124 | } 125 | 126 | encryptionKey, err := encryptionkey.New(User, Organization) 127 | if err != nil { 128 | OutputError(eris.Wrap(err, "failed to create encryption keypair")) 129 | } 130 | 131 | if name == "-" { 132 | Output(map[string]string{ 133 | "publicKey": encryptionKey.TkPublicKey, 134 | "privateKey": encryptionKey.TkPrivateKey, 135 | }) 136 | } 137 | 138 | encryptionKey.Metadata.Name = name 139 | encryptionKey.Metadata.PublicKey = encryptionKey.TkPublicKey 140 | encryptionKey.Metadata.Organization = Organization 141 | encryptionKey.Metadata.User = User 142 | 143 | if err = encryptionKeyStore.Store(name, encryptionKey); err != nil { 144 | OutputError(eris.Wrap(err, "failed to store new encryption keypair")) 145 | } 146 | 147 | localStore, ok := encryptionKeyStore.(*local.Store[*encryptionkey.Key, encryptionkey.Metadata]) 148 | if !ok { 149 | OutputError(eris.Wrap(err, "unhandled keystore type: expected *local.Store")) 150 | } 151 | 152 | Output(map[string]string{ 153 | "publicKey": encryptionKey.TkPublicKey, 154 | "publicKeyFile": localStore.PublicKeyFile(name), 155 | "privateKeyFile": localStore.PrivateKeyFile(name), 156 | }) 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/organizations.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/rotisserie/eris" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/tkhq/go-sdk/pkg/api/client/private_keys" 8 | "github.com/tkhq/go-sdk/pkg/api/models" 9 | ) 10 | 11 | var organizationsCreateName string 12 | 13 | func init() { 14 | organizationsCreateCmd.Flags().StringVar(&organizationsCreateName, "name", "", "name of the organization") 15 | 16 | organizationsCmd.AddCommand(organizationsCreateCmd) 17 | 18 | rootCmd.AddCommand(organizationsCmd) 19 | } 20 | 21 | var organizationsCmd = &cobra.Command{ 22 | Use: "organizations", 23 | Short: "Interact with organizations stored in Turnkey", 24 | Aliases: []string{"o", "org", "organization"}, 25 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 26 | basicSetup(cmd) 27 | LoadKeypair("") 28 | LoadClient() 29 | }, 30 | } 31 | 32 | var organizationsCreateCmd = &cobra.Command{ 33 | Use: "create", 34 | Short: "Create a new organization", 35 | PreRun: func(cmd *cobra.Command, args []string) { 36 | if organizationsCreateName == "" { 37 | OutputError(eris.New("--name must be specified")) 38 | } 39 | }, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | curve := models.Curve(privateKeysCreateCurve) 42 | 43 | addressFormats := make([]models.AddressFormat, len(privateKeysCreateAddressFormats)) 44 | 45 | for n, f := range privateKeysCreateAddressFormats { 46 | addressFormats[n] = models.AddressFormat(f) 47 | } 48 | 49 | params := private_keys.NewCreatePrivateKeysParams() 50 | params.SetBody(&models.CreatePrivateKeysRequest{ 51 | OrganizationID: &Organization, 52 | Parameters: &models.CreatePrivateKeysIntentV2{ 53 | PrivateKeys: []*models.PrivateKeyParams{ 54 | { 55 | AddressFormats: addressFormats, 56 | Curve: &curve, 57 | PrivateKeyName: &privateKeysCreateName, 58 | PrivateKeyTags: privateKeysCreateTags, 59 | }, 60 | }, 61 | }, 62 | }) 63 | 64 | resp, err := APIClient.V0().PrivateKeys.CreatePrivateKeys(params, APIClient.Authenticator) 65 | if err != nil { 66 | OutputError(eris.Wrap(err, "request failed")) 67 | } 68 | 69 | if !resp.IsSuccess() { 70 | OutputError(eris.Errorf("failed to create private key: %s", resp.Error())) 71 | } 72 | 73 | Output(resp.Payload) 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/private-keys.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/rotisserie/eris" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/tkhq/go-sdk/pkg/api/client/private_keys" 10 | "github.com/tkhq/go-sdk/pkg/api/models" 11 | "github.com/tkhq/go-sdk/pkg/encryptionkey" 12 | "github.com/tkhq/go-sdk/pkg/util" 13 | ) 14 | 15 | var ( 16 | privateKeysCreateAddressFormats []string 17 | privateKeysCreateCurve string 18 | privateKeysCreateName string 19 | privateKeysCreateTags []string 20 | privateKeyNameOrID string 21 | ) 22 | 23 | func init() { 24 | privateKeysCreateCmd.Flags().StringSliceVar(&privateKeysCreateAddressFormats, "address-format", nil, "address format(s) for private key. For a list of formats, use 'turnkey address-formats list'.") 25 | privateKeysCreateCmd.Flags().StringVar(&privateKeysCreateCurve, "curve", "", "curve to use for the generation of the private key. For a list of available curves, use 'turnkey curves list'.") 26 | privateKeysCreateCmd.Flags().StringVar(&privateKeysCreateName, "name", "", "name to be applied to the private key") 27 | privateKeysCreateCmd.Flags().StringSliceVar(&privateKeysCreateTags, "tag", make([]string, 0), "tag(s) to be applied to the private key") 28 | 29 | privateKeyExportCmd.Flags().StringVar(&privateKeyNameOrID, "id", "", "name or ID of private key to export.") 30 | privateKeyExportCmd.Flags().StringVar(&exportBundlePath, "export-bundle-output", "", "filepath to write the export bundle to.") 31 | 32 | privateKeyInitImportCmd.Flags().StringVar(&User, "user", "", "ID of user to importing the private key") 33 | privateKeyInitImportCmd.Flags().StringVar(&importBundlePath, "import-bundle-output", "", "filepath to write the import bundle to.") 34 | 35 | privateKeyImportCmd.Flags().StringVar(&User, "user", "", "ID of user to importing the private key") 36 | privateKeyImportCmd.Flags().StringVar(&privateKeysCreateName, "name", "", "name to be applied to the private key.") 37 | privateKeyImportCmd.Flags().StringVar(&encryptedBundlePath, "encrypted-bundle-input", "", "filepath to read the encrypted bundle from.") 38 | privateKeyImportCmd.Flags().StringSliceVar(&privateKeysCreateAddressFormats, "address-format", nil, "address format(s) for private key. For a list of formats, use 'turnkey address-formats list'.") 39 | privateKeyImportCmd.Flags().StringVar(&privateKeysCreateCurve, "curve", "", "curve to use for the generation of the private key. For a list of available curves, use 'turnkey curves list'.") 40 | 41 | privateKeysCmd.AddCommand(privateKeysCreateCmd) 42 | privateKeysCmd.AddCommand(privateKeysListCmd) 43 | privateKeysCmd.AddCommand(privateKeyExportCmd) 44 | privateKeysCmd.AddCommand(privateKeyInitImportCmd) 45 | privateKeysCmd.AddCommand(privateKeyImportCmd) 46 | 47 | rootCmd.AddCommand(privateKeysCmd) 48 | } 49 | 50 | var privateKeysCmd = &cobra.Command{ 51 | Use: "private-keys", 52 | Short: "Interact with private keys", 53 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 54 | basicSetup(cmd) 55 | LoadKeypair("") 56 | LoadClient() 57 | }, 58 | Aliases: []string{"pk"}, 59 | } 60 | 61 | var privateKeysCreateCmd = &cobra.Command{ 62 | Use: "create", 63 | Short: "Create a new private key", 64 | PreRun: func(cmd *cobra.Command, args []string) { 65 | if len(privateKeysCreateAddressFormats) < 1 { 66 | OutputError(eris.New("--address-format must not be empty")) 67 | } 68 | 69 | if privateKeysCreateCurve == "" { 70 | OutputError(eris.New("--curve must be specified")) 71 | } 72 | 73 | if privateKeysCreateName == "" { 74 | OutputError(eris.New("--name must be specified")) 75 | } 76 | }, 77 | Run: func(cmd *cobra.Command, args []string) { 78 | curve := models.Curve(privateKeysCreateCurve) 79 | 80 | if curve == Help { 81 | Output(models.CurveEnum) 82 | return 83 | } 84 | 85 | addressFormats := make([]models.AddressFormat, len(privateKeysCreateAddressFormats)) 86 | 87 | for n, f := range privateKeysCreateAddressFormats { 88 | if f == Help { 89 | Output(models.AddressFormatEnum) 90 | return 91 | } 92 | 93 | addressFormats[n] = models.AddressFormat(f) 94 | } 95 | 96 | activity := string(models.ActivityTypeCreatePrivateKeysV2) 97 | 98 | params := private_keys.NewCreatePrivateKeysParams() 99 | params.SetBody(&models.CreatePrivateKeysRequest{ 100 | OrganizationID: &Organization, 101 | Parameters: &models.CreatePrivateKeysIntentV2{ 102 | PrivateKeys: []*models.PrivateKeyParams{ 103 | { 104 | AddressFormats: addressFormats, 105 | Curve: &curve, 106 | PrivateKeyName: &privateKeysCreateName, 107 | PrivateKeyTags: privateKeysCreateTags, 108 | }, 109 | }, 110 | }, 111 | TimestampMs: util.RequestTimestamp(), 112 | Type: &activity, 113 | }) 114 | 115 | if err := params.Body.Validate(nil); err != nil { 116 | OutputError(eris.Wrap(err, "request validation failed")) 117 | } 118 | 119 | resp, err := APIClient.V0().PrivateKeys.CreatePrivateKeys(params, APIClient.Authenticator) 120 | if err != nil { 121 | OutputError(eris.Wrap(err, "request failed")) 122 | } 123 | 124 | if !resp.IsSuccess() { 125 | OutputError(eris.Errorf("failed to create private key: %s", resp.Error())) 126 | } 127 | 128 | Output(resp.Payload) 129 | }, 130 | } 131 | 132 | var privateKeysListCmd = &cobra.Command{ 133 | Use: "list", 134 | Short: "Return private keys for the organization", 135 | Run: func(cmd *cobra.Command, args []string) { 136 | params := private_keys.NewGetPrivateKeysParams() 137 | 138 | params.SetBody(&models.GetPrivateKeysRequest{ 139 | OrganizationID: &Organization, 140 | }) 141 | 142 | if err := params.Body.Validate(nil); err != nil { 143 | OutputError(eris.Wrap(err, "request validation failed")) 144 | } 145 | 146 | resp, err := APIClient.V0().PrivateKeys.GetPrivateKeys(params, APIClient.Authenticator) 147 | if err != nil { 148 | OutputError(eris.Wrap(err, "request failed")) 149 | } 150 | 151 | if !resp.IsSuccess() { 152 | OutputError(eris.Errorf("failed to list private keys: %s", resp.Error())) 153 | } 154 | 155 | Output(resp.Payload) 156 | }, 157 | } 158 | 159 | var privateKeyExportCmd = &cobra.Command{ 160 | Use: "export", 161 | Short: "Export a private key", 162 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 163 | basicSetup(cmd) 164 | LoadKeypair("") 165 | LoadEncryptionKeypair("") 166 | LoadClient() 167 | }, 168 | PreRun: func(cmd *cobra.Command, args []string) { 169 | if privateKeyNameOrID == "" { 170 | OutputError(eris.New("--id must be specified")) 171 | } 172 | 173 | if EncryptionKeyName == "" { 174 | OutputError(eris.New("--encryption-key-name must be specified")) 175 | } 176 | 177 | if exportBundlePath == "" { 178 | OutputError(eris.New("--export-bundle-output must be specified")) 179 | } 180 | }, 181 | Run: func(cmd *cobra.Command, args []string) { 182 | privateKey, err := lookupPrivateKey(privateKeyNameOrID) 183 | if err != nil { 184 | OutputError(eris.Wrap(err, "failed to lookup private key")) 185 | } 186 | 187 | tkPublicKey := EncryptionKeypair.GetPublicKey() 188 | kemPublicKey, err := encryptionkey.DecodeTurnkeyPublicKey(tkPublicKey) 189 | if err != nil { 190 | OutputError(eris.Wrap(err, "failed to decode encryption public key")) 191 | } 192 | kemPublicKeyBytes, err := (*kemPublicKey).MarshalBinary() 193 | if err != nil { 194 | OutputError(eris.Wrap(err, "failed to marshal encryption public key")) 195 | } 196 | targetPublicKey := hex.EncodeToString(kemPublicKeyBytes) 197 | 198 | activity := string(models.ActivityTypeExportPrivateKey) 199 | 200 | params := private_keys.NewExportPrivateKeyParams() 201 | params.SetBody(&models.ExportPrivateKeyRequest{ 202 | OrganizationID: &Organization, 203 | Parameters: &models.ExportPrivateKeyIntent{ 204 | PrivateKeyID: privateKey.PrivateKeyID, 205 | TargetPublicKey: &targetPublicKey, 206 | }, 207 | TimestampMs: util.RequestTimestamp(), 208 | Type: &activity, 209 | }) 210 | 211 | if err := params.Body.Validate(nil); err != nil { 212 | OutputError(eris.Wrap(err, "request validation failed")) 213 | } 214 | 215 | resp, err := APIClient.V0().PrivateKeys.ExportPrivateKey(params, APIClient.Authenticator) 216 | if err != nil { 217 | OutputError(eris.Wrap(err, "request failed")) 218 | } 219 | 220 | if !resp.IsSuccess() { 221 | OutputError(eris.Errorf("failed to export private key: %s", resp.Error())) 222 | } 223 | 224 | exportBundle := resp.Payload.Activity.Result.ExportPrivateKeyResult.ExportBundle 225 | err = writeFile(*exportBundle, exportBundlePath) 226 | if err != nil { 227 | OutputError(eris.Wrap(err, "failed to write export bundle to file")) 228 | } 229 | 230 | exportedPrivateKeyID := resp.Payload.Activity.Result.ExportPrivateKeyResult.PrivateKeyID 231 | Output(exportedPrivateKeyID) 232 | }, 233 | } 234 | 235 | var privateKeyInitImportCmd = &cobra.Command{ 236 | Use: "init-import", 237 | Short: "Initialize private key import", 238 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 239 | basicSetup(cmd) 240 | LoadKeypair("") 241 | LoadEncryptionKeypair("") 242 | LoadClient() 243 | }, 244 | PreRun: func(cmd *cobra.Command, args []string) { 245 | if User == "" { 246 | OutputError(eris.New("--user must be specified")) 247 | } 248 | 249 | if importBundlePath == "" { 250 | OutputError(eris.New("--import-bundle-output must be specified")) 251 | } 252 | }, 253 | Run: func(cmd *cobra.Command, args []string) { 254 | activity := string(models.ActivityTypeInitImportPrivateKey) 255 | 256 | params := private_keys.NewInitImportPrivateKeyParams() 257 | params.SetBody(&models.InitImportPrivateKeyRequest{ 258 | OrganizationID: &Organization, 259 | Parameters: &models.InitImportPrivateKeyIntent{ 260 | UserID: &User, 261 | }, 262 | TimestampMs: util.RequestTimestamp(), 263 | Type: &activity, 264 | }) 265 | 266 | if err := params.Body.Validate(nil); err != nil { 267 | OutputError(eris.Wrap(err, "request validation failed")) 268 | } 269 | 270 | resp, err := APIClient.V0().PrivateKeys.InitImportPrivateKey(params, APIClient.Authenticator) 271 | if err != nil { 272 | OutputError(eris.Wrap(err, "request failed")) 273 | } 274 | 275 | if !resp.IsSuccess() { 276 | OutputError(eris.Errorf("failed to initialize private key import: %s", resp.Error())) 277 | } 278 | 279 | importBundle := resp.Payload.Activity.Result.InitImportPrivateKeyResult.ImportBundle 280 | err = writeFile(*importBundle, importBundlePath) 281 | if err != nil { 282 | OutputError(eris.Wrap(err, "failed to write import bundle to file")) 283 | } 284 | }, 285 | } 286 | 287 | var privateKeyImportCmd = &cobra.Command{ 288 | Use: "import", 289 | Short: "Import a private key", 290 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 291 | basicSetup(cmd) 292 | LoadKeypair("") 293 | LoadEncryptionKeypair("") 294 | LoadClient() 295 | }, 296 | PreRun: func(cmd *cobra.Command, args []string) { 297 | if User == "" { 298 | OutputError(eris.New("--user must be specified")) 299 | } 300 | 301 | if encryptedBundlePath == "" { 302 | OutputError(eris.New("--encrypted-bundle-input must be specified")) 303 | } 304 | 305 | if len(privateKeysCreateAddressFormats) < 1 { 306 | OutputError(eris.New("--address-format must not be empty")) 307 | } 308 | 309 | if privateKeysCreateCurve == "" { 310 | OutputError(eris.New("--curve must be specified")) 311 | } 312 | 313 | if privateKeysCreateName == "" { 314 | OutputError(eris.New("--name must be specified")) 315 | } 316 | }, 317 | Run: func(cmd *cobra.Command, args []string) { 318 | encryptedBundle, err := readFile(encryptedBundlePath) 319 | if err != nil { 320 | OutputError(err) 321 | } 322 | 323 | curve := models.Curve(privateKeysCreateCurve) 324 | 325 | if curve == Help { 326 | Output(models.CurveEnum) 327 | return 328 | } 329 | 330 | addressFormats := make([]models.AddressFormat, len(privateKeysCreateAddressFormats)) 331 | 332 | for n, f := range privateKeysCreateAddressFormats { 333 | if f == Help { 334 | Output(models.AddressFormatEnum) 335 | return 336 | } 337 | 338 | addressFormats[n] = models.AddressFormat(f) 339 | } 340 | 341 | activity := string(models.ActivityTypeImportPrivateKey) 342 | 343 | params := private_keys.NewImportPrivateKeyParams() 344 | params.SetBody(&models.ImportPrivateKeyRequest{ 345 | OrganizationID: &Organization, 346 | Parameters: &models.ImportPrivateKeyIntent{ 347 | UserID: &User, 348 | PrivateKeyName: &privateKeysCreateName, 349 | EncryptedBundle: &encryptedBundle, 350 | Curve: &curve, 351 | AddressFormats: addressFormats, 352 | }, 353 | TimestampMs: util.RequestTimestamp(), 354 | Type: &activity, 355 | }) 356 | 357 | if err := params.Body.Validate(nil); err != nil { 358 | OutputError(eris.Wrap(err, "request validation failed")) 359 | } 360 | 361 | resp, err := APIClient.V0().PrivateKeys.ImportPrivateKey(params, APIClient.Authenticator) 362 | if err != nil { 363 | OutputError(eris.Wrap(err, "request failed")) 364 | } 365 | 366 | if !resp.IsSuccess() { 367 | OutputError(eris.Errorf("failed to import private key: %s", resp.Error())) 368 | } 369 | 370 | Output(resp.Payload) 371 | }, 372 | } 373 | 374 | func lookupPrivateKey(nameOrID string) (*models.PrivateKey, error) { 375 | params := private_keys.NewGetPrivateKeysParams() 376 | 377 | params.SetBody(&models.GetPrivateKeysRequest{ 378 | OrganizationID: &Organization, 379 | }) 380 | 381 | if err := params.Body.Validate(nil); err != nil { 382 | OutputError(eris.Wrap(err, "request validation failed")) 383 | } 384 | 385 | resp, err := APIClient.V0().PrivateKeys.GetPrivateKeys(params, APIClient.Authenticator) 386 | if err != nil { 387 | OutputError(eris.Wrap(err, "request failed")) 388 | } 389 | 390 | if !resp.IsSuccess() { 391 | OutputError(eris.Errorf("failed to list private keys: %s", resp.Error())) 392 | } 393 | 394 | for _, privateKey := range resp.Payload.PrivateKeys { 395 | if *privateKey.PrivateKeyName == nameOrID || *privateKey.PrivateKeyID == nameOrID { 396 | return privateKey, nil 397 | } 398 | } 399 | 400 | return nil, eris.Errorf("private key %q not found in list of private keys", nameOrID) 401 | } 402 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/raw.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/rotisserie/eris" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/tkhq/go-sdk/pkg/api/client/signing" 8 | "github.com/tkhq/go-sdk/pkg/api/models" 9 | "github.com/tkhq/go-sdk/pkg/util" 10 | ) 11 | 12 | var ( 13 | rawSigner string 14 | rawSignPayloadEncoding string 15 | rawSignHashFunction string 16 | rawSignPayload string 17 | ) 18 | 19 | func init() { 20 | rawSignCmd.Flags().StringVarP(&rawSigner, "signer", "s", "", "wallet account address, private key address, or private key ID") 21 | rawSignCmd.Flags().StringVar(&rawSignPayloadEncoding, "payload-encoding", 22 | string(models.PayloadEncodingTextUTF8), "encoding of payload") 23 | rawSignCmd.Flags().StringVar(&rawSignHashFunction, "hash-function", 24 | string(models.HashFunctionSha256), "hash function") 25 | rawSignCmd.Flags().StringVar(&rawSignPayload, "payload", 26 | "", "payload to be signed") 27 | 28 | rawCmd.AddCommand(rawSignCmd) 29 | 30 | rootCmd.AddCommand(rawCmd) 31 | } 32 | 33 | var rawCmd = &cobra.Command{ 34 | Use: "raw", 35 | Short: "Send low-level (raw) requests to the Turnkey API", 36 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 37 | basicSetup(cmd) 38 | LoadKeypair("") 39 | LoadClient() 40 | }, 41 | } 42 | 43 | var rawSignCmd = &cobra.Command{ 44 | Use: "sign", 45 | Short: "Sign a raw payload", 46 | Run: func(cmd *cobra.Command, args []string) { 47 | payloadEncoding := models.PayloadEncoding(rawSignPayloadEncoding) 48 | hashFunction := models.HashFunction(rawSignHashFunction) 49 | 50 | payload, err := ParameterToString(rawSignPayload) 51 | if err != nil { 52 | OutputError(eris.Wrap(err, "failed to read payload")) 53 | } 54 | 55 | activityType := string(models.ActivityTypeSignRawPayloadV2) 56 | 57 | params := signing.NewSignRawPayloadParams().WithBody( 58 | &models.SignRawPayloadRequest{ 59 | OrganizationID: &Organization, 60 | Parameters: &models.SignRawPayloadIntentV2{ 61 | SignWith: &rawSigner, 62 | Encoding: &payloadEncoding, 63 | HashFunction: &hashFunction, 64 | Payload: &payload, 65 | }, 66 | TimestampMs: util.RequestTimestamp(), 67 | Type: &activityType, 68 | }, 69 | ) 70 | 71 | resp, err := APIClient.V0().Signing.SignRawPayload(params, APIClient.Authenticator) 72 | if err != nil { 73 | OutputError(eris.Wrap(err, "request failed")) 74 | } 75 | 76 | if !resp.IsSuccess() { 77 | OutputError(eris.Errorf("failed to sign raw payload: %s", resp.Error())) 78 | } 79 | 80 | Output(resp.Payload) 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/request.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "regexp" 10 | 11 | "github.com/rotisserie/eris" 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/tkhq/go-sdk/pkg/apikey" 15 | ) 16 | 17 | var ( 18 | requestPath, requestBody string 19 | requestNoPost bool 20 | ) 21 | 22 | func init() { 23 | makeRequest.Flags().StringVar(&requestBody, "body", "-", "body of the request, which can be '-' to indicate stdin or be prefixed with '@' to indicate a source filename") 24 | makeRequest.Flags().BoolVar(&requestNoPost, "no-post", false, `generates the stamp and displays 25 | the cURL command to use in order to perform this action, 26 | but does NOT post the request to the API server`) 27 | makeRequest.Flags().StringVar(&requestPath, "path", "", "path for the API request") 28 | 29 | rootCmd.AddCommand(makeRequest) 30 | } 31 | 32 | var makeRequest = &cobra.Command{ 33 | Use: "request", 34 | Short: `Given a request body, generate a stamp for it, and send it to the Turnkey API server. 35 | See options for alternate behavior, such as previewing but not sending the request.`, 36 | Aliases: []string{"req", "r"}, 37 | PreRun: func(cmd *cobra.Command, args []string) { 38 | basicSetup(cmd) 39 | LoadKeypair("") 40 | }, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | protocol := "https" 43 | if pattern := regexp.MustCompile(`^localhost:\d+$`); pattern.MatchString(apiHost) { 44 | protocol = "http" 45 | } 46 | 47 | bodyReader, err := ParameterToReader(requestBody) 48 | if err != nil { 49 | OutputError(eris.Wrap(err, "failed to process request body")) 50 | } 51 | 52 | body, err := io.ReadAll(bodyReader) 53 | if err != nil { 54 | OutputError(eris.Wrap(err, "failed to read message body")) 55 | } 56 | 57 | stamp, err := apikey.Stamp(body, APIKeypair) 58 | if err != nil { 59 | OutputError(eris.Wrap(err, "failed to produce a valid API stamp")) 60 | } 61 | 62 | if requestNoPost { 63 | Output(map[string]string{ 64 | "message": string(body), 65 | "stamp": stamp, 66 | "curlCommand": generateCurlCommand(apiHost, requestPath, body, stamp), 67 | }) 68 | } 69 | 70 | response, err := post(cmd.Context(), protocol, apiHost, requestPath, body, stamp) 71 | if err != nil { 72 | OutputError(eris.Wrap(err, "failed to post request")) 73 | } 74 | 75 | defer response.Body.Close() //nolint: errcheck 76 | 77 | responseBodyBytes, err := io.ReadAll(response.Body) 78 | if err != nil { 79 | OutputError(&ResponseError{ 80 | Code: response.StatusCode, 81 | Text: response.Status, 82 | }) 83 | } 84 | 85 | if response.StatusCode != http.StatusOK { 86 | OutputError(&ResponseError{ 87 | Code: response.StatusCode, 88 | Text: string(responseBodyBytes), 89 | }) 90 | } 91 | 92 | Output(responseBodyBytes) 93 | }, 94 | } 95 | 96 | func generateCurlCommand(host, path string, body []byte, stamp string) string { 97 | return fmt.Sprintf("curl -X POST -d'%s' -H'%s' -v 'https://%s%s'", body, stampHeader(stamp), host, path) 98 | } 99 | 100 | func stampHeader(stamp string) string { 101 | return fmt.Sprintf("X-Stamp: %s", stamp) 102 | } 103 | 104 | func post(ctx context.Context, protocol string, host string, path string, body []byte, stamp string) (*http.Response, error) { 105 | url := fmt.Sprintf("%s://%s%s", protocol, host, path) 106 | 107 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) 108 | if err != nil { 109 | return nil, eris.Wrap(err, "error while creating HTTP POST request") 110 | } 111 | 112 | req.Header.Set("X-Stamp", stamp) 113 | 114 | client := http.Client{} 115 | 116 | response, err := client.Do(req) 117 | if err != nil { 118 | return nil, eris.Wrap(err, "error while sending HTTP POST request") 119 | } 120 | 121 | return response, nil 122 | } 123 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/root.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/fs" 12 | "math/big" 13 | "os" 14 | "path/filepath" 15 | "runtime" 16 | "strings" 17 | 18 | "github.com/rotisserie/eris" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/tkhq/go-sdk/pkg/apikey" 22 | "github.com/tkhq/go-sdk/pkg/encryptionkey" 23 | "github.com/tkhq/go-sdk/pkg/store" 24 | "github.com/tkhq/go-sdk/pkg/store/local" 25 | ) 26 | 27 | var ( 28 | apiKeysDirectory string 29 | 30 | encryptionKeysDirectory string 31 | 32 | apiKeyStore store.Store[*apikey.Key, apikey.Metadata] 33 | 34 | encryptionKeyStore store.Store[*encryptionkey.Key, encryptionkey.Metadata] 35 | 36 | // ApiKeyName is the name of the key with which we are operating. 37 | ApiKeyName string 38 | 39 | // EncryptionKeyName is the name of the key with which we are operating. 40 | EncryptionKeyName string 41 | 42 | apiHost string 43 | 44 | // Organization is the organization ID to interact with. 45 | Organization string 46 | ) 47 | 48 | // Turnkey Signer enclave's quorum public key. 49 | const signerProductionPublicKey = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" 50 | 51 | func init() { 52 | rootCmd.PersistentFlags().StringVarP(&apiKeysDirectory, "keys-folder", "d", local.DefaultAPIKeysDir(), "directory in which to locate API keys") 53 | // todo(olivia): create default keys dir for encryption-keys 54 | rootCmd.PersistentFlags().StringVar(&encryptionKeysDirectory, "encryption-keys-folder", local.DefaultEncryptionKeysDir(), "directory in which to locate encryption keys") 55 | rootCmd.PersistentFlags().StringVarP(&ApiKeyName, "key-name", "k", "default", "name of API key with which to interact with the Turnkey API service") 56 | rootCmd.PersistentFlags().StringVar(&EncryptionKeyName, "encryption-key-name", "default", "name of encryption key with which to interact with the Turnkey API service") 57 | rootCmd.PersistentFlags().StringVar(&apiHost, "host", "api.turnkey.com", "hostname of the API server") 58 | 59 | rootCmd.PersistentFlags().StringVar(&Organization, "organization", "", "organization ID to be used") 60 | } 61 | 62 | func basicSetup(cmd *cobra.Command) { 63 | // No non-JSON-formatted output should flow over stdin; thus change 64 | // output for usage messages to stderr. 65 | cmd.SetOut(os.Stderr) 66 | 67 | err := detectAndMoveDeprecatedDefaultKeysDirOnMacOs() 68 | if err != nil { 69 | OutputError(err) 70 | } 71 | 72 | if apiKeyStore == nil { 73 | localApiKeyStore := local.New[*apikey.Key, apikey.Metadata]() 74 | 75 | if err := localApiKeyStore.SetAPIKeysDirectory(apiKeysDirectory); err != nil { 76 | OutputError(eris.Wrap(err, "failed to obtain API key storage location")) 77 | } 78 | 79 | apiKeyStore = localApiKeyStore 80 | } 81 | 82 | if encryptionKeyStore == nil { 83 | localEncryptionKeyStore := local.New[*encryptionkey.Key, encryptionkey.Metadata]() 84 | 85 | if err := localEncryptionKeyStore.SetEncryptionKeysDirectory(encryptionKeysDirectory); err != nil { 86 | OutputError(eris.Wrap(err, "failed to obtain encryption key storage location")) 87 | } 88 | 89 | encryptionKeyStore = localEncryptionKeyStore 90 | } 91 | } 92 | 93 | // Execute runs the cobra command for the Turnkey CLI. 94 | func Execute() error { 95 | return rootCmd.Execute() 96 | } 97 | 98 | var rootCmd = &cobra.Command{ 99 | Use: "turnkey", 100 | Short: "turnkey is the Turnkey CLI", 101 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 102 | basicSetup(cmd) 103 | }, 104 | } 105 | 106 | // ParameterToReader converts a commandline parameter to an io.Reader based on its syntax. 107 | // Values of "-" return stdin. 108 | // Values beginning with "@" return the file with name following the "@". 109 | // Other values are interpreted literally. 110 | func ParameterToReader(param string) (io.Reader, error) { 111 | if param == "-" { 112 | return os.Stdin, nil 113 | } 114 | 115 | if strings.HasPrefix(param, "@") { 116 | return os.Open(strings.TrimPrefix(param, "@")) 117 | } 118 | 119 | return bytes.NewReader([]byte(param)), nil 120 | } 121 | 122 | // ParameterToString processes a commandline parameter with ParameterToReader, reads it in, and then returns a string with its contents. 123 | // See ParameterToReader for conversion details. 124 | func ParameterToString(param string) (string, error) { 125 | payloadReader, err := ParameterToReader(param) 126 | if err != nil { 127 | return "", eris.Wrap(err, "failed to process payload") 128 | } 129 | 130 | buf := new(bytes.Buffer) 131 | 132 | if _, err := buf.ReadFrom(payloadReader); err != nil { 133 | return "", eris.Wrap(err, "failed to read payload data") 134 | } 135 | 136 | return buf.String(), nil 137 | } 138 | 139 | func detectAndMoveDeprecatedDefaultKeysDirOnMacOs() error { 140 | if runtime.GOOS != "darwin" { 141 | return nil 142 | } 143 | 144 | deprecatedDir := local.DeprecatedDefaultAPIKeysDir() 145 | if deprecatedDir == "" { 146 | return nil 147 | } 148 | 149 | newDir := local.DefaultAPIKeysDir() 150 | fmt.Printf("Legacy keys directory detected; will migrate keys to new location\n- Legacy: %s\n- New: %s\n\n", deprecatedDir, newDir) 151 | 152 | err := filepath.WalkDir(deprecatedDir, func(path string, d fs.DirEntry, err error) error { 153 | if err != nil { 154 | return err 155 | } 156 | 157 | // Skip directories 158 | if d.IsDir() { 159 | return nil 160 | } 161 | 162 | relativeFilePath, err := filepath.Rel(deprecatedDir, path) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | destFilePath := filepath.Join(newDir, relativeFilePath) 168 | 169 | err = safeRename(path, destFilePath) 170 | 171 | if err != nil { 172 | return err 173 | } 174 | 175 | fmt.Printf("Moved `%s` to %s\n", relativeFilePath, destFilePath) 176 | 177 | return nil 178 | }) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | err = os.RemoveAll(deprecatedDir) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | fmt.Println("") 189 | fmt.Println("Successfully migrated legacy keys directory.") 190 | fmt.Println("") 191 | 192 | return nil 193 | } 194 | 195 | // Like `os.Rename(...)`, but does not allow overwriting 196 | func safeRename(oldPath string, newPath string) error { 197 | exists, err := checkExists(newPath) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | if exists { 203 | return eris.Errorf("target path already exists: %s", newPath) 204 | } 205 | 206 | err = os.Rename(oldPath, newPath) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | return nil 212 | } 213 | 214 | // Reads the content from the given file path and trims whitespace. 215 | func readFile(path string) (string, error) { 216 | content, err := os.ReadFile(path) 217 | if err != nil { 218 | return "", eris.Wrap(err, "error reading file") 219 | } 220 | 221 | return strings.TrimSpace(string(content)), nil 222 | } 223 | 224 | // Writes the given content to a file at the specified path. 225 | func writeFile(content string, path string) error { 226 | err := os.WriteFile(path, []byte(content), 0644) 227 | if err != nil { 228 | return eris.Wrap(err, "error writing file") 229 | } 230 | return nil 231 | } 232 | 233 | func checkExists(path string) (bool, error) { 234 | _, err := os.Stat(path) 235 | if errors.Is(err, fs.ErrNotExist) { 236 | return false, nil 237 | } 238 | 239 | if err != nil { 240 | return false, err 241 | } 242 | 243 | return true, nil 244 | } 245 | 246 | // Convert a hex-encoded string to an ECDSA P-256 public key. 247 | // This key is used in encryption and decryption of data transferred to 248 | // and from Turnkey secure enclaves. 249 | func hexToPublicKey(hexString string) (*ecdsa.PublicKey, error) { 250 | publicKeyBytes, err := hex.DecodeString(hexString) 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | // second half is the public key bytes for the enclave quorum encryption key 256 | if len(publicKeyBytes) != 65 { 257 | return nil, eris.Errorf("invalid public key length. Expected 65 bytes but got %d (hex string: \"%s\")", len(publicKeyBytes), publicKeyBytes) 258 | } 259 | 260 | // init curve instance 261 | curve := elliptic.P256() 262 | 263 | // curve's bitsize converted to length in bytes 264 | byteLen := (curve.Params().BitSize + 7) / 8 265 | 266 | // ensure the public key bytes have the correct length 267 | if len(publicKeyBytes) != 1+2*byteLen { 268 | return nil, eris.New("invalid encryption public key length") 269 | } 270 | 271 | // extract X and Y coordinates from the public key bytes 272 | // ignore first byte (prefix) 273 | x := new(big.Int).SetBytes(publicKeyBytes[1 : 1+byteLen]) 274 | y := new(big.Int).SetBytes(publicKeyBytes[1+byteLen:]) 275 | 276 | return &ecdsa.PublicKey{ 277 | Curve: curve, 278 | X: x, 279 | Y: y, 280 | }, nil 281 | } 282 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/transaction-types.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/tkhq/go-sdk/pkg/api/models" 7 | ) 8 | 9 | func init() { 10 | transactionTypesCmd.AddCommand(transactionTypesListCmd) 11 | 12 | rootCmd.AddCommand(transactionTypesCmd) 13 | } 14 | 15 | var transactionTypesCmd = &cobra.Command{ 16 | Use: "transaction-types", 17 | Short: "Interact with the available transaction types", 18 | } 19 | 20 | var transactionTypesListCmd = &cobra.Command{ 21 | Use: "list", 22 | Short: "Return the available transaction types", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | Output(models.TransactionTypeEnum) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/version.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var versionString string 10 | 11 | func init() { 12 | rootCmd.AddCommand(versionCmd) 13 | } 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "display build and version information", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Println(versionString) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/cmd/turnkey/pkg/wallets.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/rotisserie/eris" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/tkhq/go-sdk/pkg/api/client/wallets" 11 | "github.com/tkhq/go-sdk/pkg/api/models" 12 | "github.com/tkhq/go-sdk/pkg/encryptionkey" 13 | "github.com/tkhq/go-sdk/pkg/util" 14 | ) 15 | 16 | var ( 17 | walletNameOrID string 18 | walletAccountAddressFormat string 19 | walletAccountCurve string 20 | walletAccountPathFormat string 21 | walletAccountPath string 22 | walletAccountAddress string 23 | ) 24 | 25 | func init() { 26 | walletCreateCmd.Flags().StringVar(&walletNameOrID, "name", "", "name to be applied to the wallet.") 27 | 28 | walletExportCmd.Flags().StringVar(&walletNameOrID, "name", "", "name or ID of wallet to export.") 29 | walletExportCmd.Flags().StringVar(&exportBundlePath, "export-bundle-output", "", "filepath to write the export bundle to.") 30 | 31 | walletAccountExportCmd.Flags().StringVar(&walletAccountAddress, "address", "", "address of wallet account to export.") 32 | walletAccountExportCmd.Flags().StringVar(&exportBundlePath, "export-bundle-output", "", "filepath to write the export bundle to.") 33 | 34 | walletInitImportCmd.Flags().StringVar(&User, "user", "", "ID of user to importing the wallet") 35 | walletInitImportCmd.Flags().StringVar(&importBundlePath, "import-bundle-output", "", "filepath to write the import bundle to.") 36 | 37 | walletImportCmd.Flags().StringVar(&User, "user", "", "ID of user to importing the wallet") 38 | walletImportCmd.Flags().StringVar(&walletNameOrID, "name", "", "name to be applied to the wallet.") 39 | walletImportCmd.Flags().StringVar(&encryptedBundlePath, "encrypted-bundle-input", "", "filepath to read the encrypted bundle from.") 40 | 41 | walletAccountsListCmd.Flags().StringVar(&walletNameOrID, "wallet", "", "name or ID of wallet used to fetch accounts.") 42 | 43 | walletAccountCreateCmd.Flags().StringVar(&walletNameOrID, "wallet", "", "name or ID of wallet used for account creation.") 44 | walletAccountCreateCmd.Flags().StringVar(&walletAccountAddressFormat, "address-format", "", "address format for account. For a list of formats, use 'turnkey address-formats list'.") 45 | walletAccountCreateCmd.Flags().StringVar(&walletAccountCurve, "curve", "", "curve for account. For a list of curves, use 'turnkey curves list'. If unset, will predict based on address format.") 46 | walletAccountCreateCmd.Flags().StringVar(&walletAccountPathFormat, "path-format", string(models.PathFormatBip32), "the derivation path format for account.") 47 | walletAccountCreateCmd.Flags().StringVar(&walletAccountPath, "path", "", "the derivation path for account. If unset, will predict next path.") 48 | 49 | walletAccountsCmd.AddCommand(walletAccountsListCmd) 50 | walletAccountsCmd.AddCommand(walletAccountCreateCmd) 51 | walletAccountsCmd.AddCommand(walletAccountExportCmd) 52 | walletsCmd.AddCommand(walletCreateCmd) 53 | walletsCmd.AddCommand(walletsListCmd) 54 | walletsCmd.AddCommand(walletExportCmd) 55 | walletsCmd.AddCommand(walletInitImportCmd) 56 | walletsCmd.AddCommand(walletImportCmd) 57 | walletsCmd.AddCommand(walletAccountsCmd) 58 | 59 | rootCmd.AddCommand(walletsCmd) 60 | } 61 | 62 | var walletsCmd = &cobra.Command{ 63 | Use: "wallets", 64 | Short: "Interact with wallets", 65 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 66 | basicSetup(cmd) 67 | LoadKeypair("") 68 | LoadClient() 69 | }, 70 | Aliases: []string{}, 71 | } 72 | 73 | var walletCreateCmd = &cobra.Command{ 74 | Use: "create", 75 | Short: "Create a new wallet", 76 | PreRun: func(cmd *cobra.Command, args []string) { 77 | if walletNameOrID == "" { 78 | OutputError(eris.New("--name must be specified")) 79 | } 80 | }, 81 | Run: func(cmd *cobra.Command, args []string) { 82 | activity := string(models.ActivityTypeCreateWallet) 83 | 84 | params := wallets.NewCreateWalletParams() 85 | params.SetBody(&models.CreateWalletRequest{ 86 | OrganizationID: &Organization, 87 | Parameters: &models.CreateWalletIntent{ 88 | WalletName: &walletNameOrID, 89 | Accounts: []*models.WalletAccountParams{}, 90 | }, 91 | TimestampMs: util.RequestTimestamp(), 92 | Type: &activity, 93 | }) 94 | 95 | if err := params.Body.Validate(nil); err != nil { 96 | OutputError(eris.Wrap(err, "request validation failed")) 97 | } 98 | 99 | resp, err := APIClient.V0().Wallets.CreateWallet(params, APIClient.Authenticator) 100 | if err != nil { 101 | OutputError(eris.Wrap(err, "request failed")) 102 | } 103 | 104 | if !resp.IsSuccess() { 105 | OutputError(eris.Errorf("failed to create wallet: %s", resp.Error())) 106 | } 107 | 108 | Output(resp.Payload) 109 | }, 110 | } 111 | 112 | var walletsListCmd = &cobra.Command{ 113 | Use: "list", 114 | Short: "Return wallets for the organization", 115 | Run: func(cmd *cobra.Command, args []string) { 116 | params := wallets.NewGetWalletsParams() 117 | 118 | params.SetBody(&models.GetWalletsRequest{ 119 | OrganizationID: &Organization, 120 | }) 121 | 122 | if err := params.Body.Validate(nil); err != nil { 123 | OutputError(eris.Wrap(err, "request validation failed")) 124 | } 125 | 126 | resp, err := APIClient.V0().Wallets.GetWallets(params, APIClient.Authenticator) 127 | if err != nil { 128 | OutputError(eris.Wrap(err, "request failed")) 129 | } 130 | 131 | if !resp.IsSuccess() { 132 | OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) 133 | } 134 | 135 | Output(resp.Payload) 136 | }, 137 | } 138 | 139 | var walletExportCmd = &cobra.Command{ 140 | Use: "export", 141 | Short: "Export a wallet", 142 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 143 | basicSetup(cmd) 144 | LoadKeypair("") 145 | LoadClient() 146 | LoadEncryptionKeypair("") 147 | }, 148 | PreRun: func(cmd *cobra.Command, args []string) { 149 | if walletNameOrID == "" { 150 | OutputError(eris.New("--name must be specified")) 151 | } 152 | 153 | if EncryptionKeyName == "" { 154 | OutputError(eris.New("--encryption-key-name must be specified")) 155 | } 156 | 157 | if exportBundlePath == "" { 158 | OutputError(eris.New("--export-bundle-output must be specified")) 159 | } 160 | }, 161 | Run: func(cmd *cobra.Command, args []string) { 162 | wallet, err := lookupWallet(walletNameOrID) 163 | if err != nil { 164 | OutputError(eris.Wrap(err, "failed to lookup wallet")) 165 | } 166 | 167 | tkPublicKey := EncryptionKeypair.GetPublicKey() 168 | kemPublicKey, err := encryptionkey.DecodeTurnkeyPublicKey(tkPublicKey) 169 | if err != nil { 170 | OutputError(eris.Wrap(err, "failed to decode encryption public key")) 171 | } 172 | kemPublicKeyBytes, err := (*kemPublicKey).MarshalBinary() 173 | if err != nil { 174 | OutputError(eris.Wrap(err, "failed to marshal encryption public key")) 175 | } 176 | targetPublicKey := hex.EncodeToString(kemPublicKeyBytes) 177 | 178 | activity := string(models.ActivityTypeExportWallet) 179 | 180 | params := wallets.NewExportWalletParams() 181 | params.SetBody(&models.ExportWalletRequest{ 182 | OrganizationID: &Organization, 183 | Parameters: &models.ExportWalletIntent{ 184 | WalletID: wallet.WalletID, 185 | TargetPublicKey: &targetPublicKey, 186 | }, 187 | TimestampMs: util.RequestTimestamp(), 188 | Type: &activity, 189 | }) 190 | 191 | if err := params.Body.Validate(nil); err != nil { 192 | OutputError(eris.Wrap(err, "request validation failed")) 193 | } 194 | 195 | resp, err := APIClient.V0().Wallets.ExportWallet(params, APIClient.Authenticator) 196 | if err != nil { 197 | OutputError(eris.Wrap(err, "request failed")) 198 | } 199 | 200 | if !resp.IsSuccess() { 201 | OutputError(eris.Errorf("failed to export wallet: %s", resp.Error())) 202 | } 203 | 204 | exportBundle := resp.Payload.Activity.Result.ExportWalletResult.ExportBundle 205 | err = writeFile(*exportBundle, exportBundlePath) 206 | if err != nil { 207 | OutputError(eris.Wrap(err, "failed to write export bundle to file")) 208 | } 209 | 210 | exportedWalletID := resp.Payload.Activity.Result.ExportWalletResult.WalletID 211 | Output(exportedWalletID) 212 | }, 213 | } 214 | 215 | var walletAccountExportCmd = &cobra.Command{ 216 | Use: "export", 217 | Short: "Export a wallet account", 218 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 219 | basicSetup(cmd) 220 | LoadKeypair("") 221 | LoadClient() 222 | LoadEncryptionKeypair("") 223 | }, 224 | PreRun: func(cmd *cobra.Command, args []string) { 225 | if walletAccountAddress == "" { 226 | OutputError(eris.New("--address must be specified")) 227 | } 228 | 229 | if EncryptionKeyName == "" { 230 | OutputError(eris.New("--encryption-key-name must be specified")) 231 | } 232 | 233 | if exportBundlePath == "" { 234 | OutputError(eris.New("--export-bundle-output must be specified")) 235 | } 236 | }, 237 | Run: func(cmd *cobra.Command, args []string) { 238 | tkPublicKey := EncryptionKeypair.GetPublicKey() 239 | kemPublicKey, err := encryptionkey.DecodeTurnkeyPublicKey(tkPublicKey) 240 | if err != nil { 241 | OutputError(eris.Wrap(err, "failed to decode encryption public key")) 242 | } 243 | kemPublicKeyBytes, err := (*kemPublicKey).MarshalBinary() 244 | if err != nil { 245 | OutputError(eris.Wrap(err, "failed to marshal encryption public key")) 246 | } 247 | targetPublicKey := hex.EncodeToString(kemPublicKeyBytes) 248 | 249 | activity := string(models.ActivityTypeExportWalletAccount) 250 | 251 | params := wallets.NewExportWalletAccountParams() 252 | params.SetBody(&models.ExportWalletAccountRequest{ 253 | OrganizationID: &Organization, 254 | Parameters: &models.ExportWalletAccountIntent{ 255 | Address: &walletAccountAddress, 256 | TargetPublicKey: &targetPublicKey, 257 | }, 258 | TimestampMs: util.RequestTimestamp(), 259 | Type: &activity, 260 | }) 261 | 262 | if err := params.Body.Validate(nil); err != nil { 263 | OutputError(eris.Wrap(err, "request validation failed")) 264 | } 265 | 266 | resp, err := APIClient.V0().Wallets.ExportWalletAccount(params, APIClient.Authenticator) 267 | if err != nil { 268 | OutputError(eris.Wrap(err, "request failed")) 269 | } 270 | 271 | if !resp.IsSuccess() { 272 | OutputError(eris.Errorf("failed to export wallet account: %s", resp.Error())) 273 | } 274 | 275 | exportBundle := resp.Payload.Activity.Result.ExportWalletAccountResult.ExportBundle 276 | if err := writeFile(*exportBundle, exportBundlePath); err != nil { 277 | OutputError(eris.Wrap(err, "failed to write export bundle to file")) 278 | } 279 | 280 | exportedWalletAccountAddress := resp.Payload.Activity.Result.ExportWalletAccountResult.Address 281 | Output(exportedWalletAccountAddress) 282 | }, 283 | } 284 | 285 | var walletInitImportCmd = &cobra.Command{ 286 | Use: "init-import", 287 | Short: "Initialize wallet import", 288 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 289 | basicSetup(cmd) 290 | LoadKeypair("") 291 | LoadClient() 292 | LoadEncryptionKeypair("") 293 | }, 294 | PreRun: func(cmd *cobra.Command, args []string) { 295 | if User == "" { 296 | OutputError(eris.New("--user must be specified")) 297 | } 298 | 299 | if importBundlePath == "" { 300 | OutputError(eris.New("--import-bundle-output must be specified")) 301 | } 302 | }, 303 | Run: func(cmd *cobra.Command, args []string) { 304 | activity := string(models.ActivityTypeInitImportWallet) 305 | 306 | params := wallets.NewInitImportWalletParams() 307 | params.SetBody(&models.InitImportWalletRequest{ 308 | OrganizationID: &Organization, 309 | Parameters: &models.InitImportWalletIntent{ 310 | UserID: &User, 311 | }, 312 | TimestampMs: util.RequestTimestamp(), 313 | Type: &activity, 314 | }) 315 | 316 | if err := params.Body.Validate(nil); err != nil { 317 | OutputError(eris.Wrap(err, "request validation failed")) 318 | } 319 | 320 | resp, err := APIClient.V0().Wallets.InitImportWallet(params, APIClient.Authenticator) 321 | if err != nil { 322 | OutputError(eris.Wrap(err, "request failed")) 323 | } 324 | 325 | if !resp.IsSuccess() { 326 | OutputError(eris.Errorf("failed to initialize wallet import: %s", resp.Error())) 327 | } 328 | 329 | importBundle := resp.Payload.Activity.Result.InitImportWalletResult.ImportBundle 330 | err = writeFile(*importBundle, importBundlePath) 331 | if err != nil { 332 | OutputError(eris.Wrap(err, "failed to write import bundle to file")) 333 | } 334 | }, 335 | } 336 | 337 | var walletImportCmd = &cobra.Command{ 338 | Use: "import", 339 | Short: "Import a wallet", 340 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 341 | basicSetup(cmd) 342 | LoadKeypair("") 343 | LoadClient() 344 | LoadEncryptionKeypair("") 345 | }, 346 | PreRun: func(cmd *cobra.Command, args []string) { 347 | if User == "" { 348 | OutputError(eris.New("--user must be specified")) 349 | } 350 | 351 | if walletNameOrID == "" { 352 | OutputError(eris.New("--name must be specified")) 353 | } 354 | 355 | if encryptedBundlePath == "" { 356 | OutputError(eris.New("--encrypted-bundle-input must be specified")) 357 | } 358 | }, 359 | Run: func(cmd *cobra.Command, args []string) { 360 | encryptedBundle, err := readFile(encryptedBundlePath) 361 | if err != nil { 362 | OutputError(err) 363 | } 364 | 365 | activity := string(models.ActivityTypeImportWallet) 366 | 367 | params := wallets.NewImportWalletParams() 368 | params.SetBody(&models.ImportWalletRequest{ 369 | OrganizationID: &Organization, 370 | Parameters: &models.ImportWalletIntent{ 371 | UserID: &User, 372 | WalletName: &walletNameOrID, 373 | EncryptedBundle: &encryptedBundle, 374 | Accounts: []*models.WalletAccountParams{}, 375 | }, 376 | TimestampMs: util.RequestTimestamp(), 377 | Type: &activity, 378 | }) 379 | 380 | if err := params.Body.Validate(nil); err != nil { 381 | OutputError(eris.Wrap(err, "request validation failed")) 382 | } 383 | 384 | resp, err := APIClient.V0().Wallets.ImportWallet(params, APIClient.Authenticator) 385 | if err != nil { 386 | OutputError(eris.Wrap(err, "request failed")) 387 | } 388 | 389 | if !resp.IsSuccess() { 390 | OutputError(eris.Errorf("failed to import wallet: %s", resp.Error())) 391 | } 392 | 393 | Output(resp.Payload) 394 | }, 395 | } 396 | 397 | var walletAccountsCmd = &cobra.Command{ 398 | Use: "accounts", 399 | Short: "Interact with wallet accounts", 400 | Aliases: []string{"acc"}, 401 | } 402 | 403 | var walletAccountCreateCmd = &cobra.Command{ 404 | Use: "create", 405 | Short: "Create a new account for a wallet", 406 | PreRun: func(cmd *cobra.Command, args []string) { 407 | if walletNameOrID == "" { 408 | OutputError(eris.New("--name must be specified")) 409 | } 410 | 411 | if walletAccountAddressFormat == "" { 412 | OutputError(eris.New("--address-format must not be empty")) 413 | } 414 | }, 415 | Run: func(cmd *cobra.Command, args []string) { 416 | wallet, err := lookupWallet(walletNameOrID) 417 | if err != nil { 418 | OutputError(eris.Wrap(err, "failed to lookup wallet")) 419 | } 420 | 421 | addressFormat := models.AddressFormat(walletAccountAddressFormat) 422 | curve := models.Curve(walletAccountCurve) 423 | pathFormat := models.PathFormat(walletAccountPathFormat) 424 | path := walletAccountPath 425 | 426 | // set standard curve, if we can, if no override 427 | if curve == "" { 428 | if standardCurve := getCurveForAddressFormat(addressFormat); standardCurve != nil { 429 | curve = *standardCurve 430 | } 431 | } 432 | 433 | // set standard path, if we can, if no override 434 | if path == "" { 435 | accounts, err := listAccountsForWallet(wallet.WalletID) 436 | if err != nil { 437 | OutputError(eris.Wrap(err, "failed to lookup wallet accounts")) 438 | } 439 | 440 | // build path map to avoid conflicts 441 | paths := make(map[string]struct{}) 442 | for _, account := range accounts { 443 | // we only need to care about accounts w/ this same address format 444 | if *account.AddressFormat != addressFormat { 445 | continue 446 | } 447 | 448 | paths[*account.Path] = struct{}{} 449 | } 450 | 451 | // find the next unused standard path 452 | for i := 0; i < len(paths)+1; i++ { 453 | if standardPath := getStandardPath(pathFormat, addressFormat, i); standardPath != nil { 454 | // we've found an unused path! 455 | if _, ok := paths[*standardPath]; !ok { 456 | path = *standardPath 457 | break 458 | } 459 | } 460 | } 461 | } 462 | 463 | activity := string(models.ActivityTypeCreateWalletAccounts) 464 | 465 | params := wallets.NewCreateWalletAccountsParams() 466 | params.SetBody(&models.CreateWalletAccountsRequest{ 467 | OrganizationID: &Organization, 468 | Parameters: &models.CreateWalletAccountsIntent{ 469 | WalletID: wallet.WalletID, 470 | Accounts: []*models.WalletAccountParams{ 471 | { 472 | AddressFormat: &addressFormat, 473 | Curve: &curve, 474 | PathFormat: &pathFormat, 475 | Path: &path, 476 | }, 477 | }, 478 | }, 479 | TimestampMs: util.RequestTimestamp(), 480 | Type: &activity, 481 | }) 482 | 483 | if err := params.Body.Validate(nil); err != nil { 484 | OutputError(eris.Wrap(err, "request validation failed")) 485 | } 486 | 487 | resp, err := APIClient.V0().Wallets.CreateWalletAccounts(params, APIClient.Authenticator) 488 | if err != nil { 489 | OutputError(eris.Wrap(err, "request failed")) 490 | } 491 | 492 | if !resp.IsSuccess() { 493 | OutputError(eris.Errorf("failed to create wallet account: %s", resp.Error())) 494 | } 495 | 496 | Output(resp.Payload) 497 | }, 498 | } 499 | 500 | var walletAccountsListCmd = &cobra.Command{ 501 | Use: "list", 502 | Short: "Return accounts for the wallet", 503 | PreRun: func(cmd *cobra.Command, args []string) { 504 | if walletNameOrID == "" { 505 | OutputError(eris.New("--name must be specified")) 506 | } 507 | }, 508 | Run: func(cmd *cobra.Command, args []string) { 509 | wallet, err := lookupWallet(walletNameOrID) 510 | if err != nil { 511 | OutputError(eris.Wrap(err, "failed to lookup wallet")) 512 | } 513 | 514 | params := wallets.NewGetWalletAccountsParams() 515 | 516 | params.SetBody(&models.GetWalletAccountsRequest{ 517 | OrganizationID: &Organization, 518 | WalletID: wallet.WalletID, 519 | }) 520 | 521 | if err := params.Body.Validate(nil); err != nil { 522 | OutputError(eris.Wrap(err, "request validation failed")) 523 | } 524 | 525 | resp, err := APIClient.V0().Wallets.GetWalletAccounts(params, APIClient.Authenticator) 526 | if err != nil { 527 | OutputError(eris.Wrap(err, "request failed")) 528 | } 529 | 530 | if !resp.IsSuccess() { 531 | OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) 532 | } 533 | 534 | Output(resp.Payload) 535 | }, 536 | } 537 | 538 | func lookupWallet(nameOrID string) (*models.Wallet, error) { 539 | params := wallets.NewGetWalletsParams() 540 | 541 | params.SetBody(&models.GetWalletsRequest{ 542 | OrganizationID: &Organization, 543 | }) 544 | 545 | if err := params.Body.Validate(nil); err != nil { 546 | OutputError(eris.Wrap(err, "request validation failed")) 547 | } 548 | 549 | resp, err := APIClient.V0().Wallets.GetWallets(params, APIClient.Authenticator) 550 | if err != nil { 551 | OutputError(eris.Wrap(err, "request failed")) 552 | } 553 | 554 | if !resp.IsSuccess() { 555 | OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) 556 | } 557 | 558 | for _, wallet := range resp.Payload.Wallets { 559 | if *wallet.WalletName == nameOrID || *wallet.WalletID == nameOrID { 560 | return wallet, nil 561 | } 562 | } 563 | 564 | return nil, eris.Errorf("wallet %q not found in list of wallets", nameOrID) 565 | } 566 | 567 | func listAccountsForWallet(walletID *string) ([]*models.WalletAccount, error) { 568 | params := wallets.NewGetWalletAccountsParams() 569 | 570 | params.SetBody(&models.GetWalletAccountsRequest{ 571 | OrganizationID: &Organization, 572 | WalletID: walletID, 573 | }) 574 | 575 | if err := params.Body.Validate(nil); err != nil { 576 | OutputError(eris.Wrap(err, "request validation failed")) 577 | } 578 | 579 | resp, err := APIClient.V0().Wallets.GetWalletAccounts(params, APIClient.Authenticator) 580 | if err != nil { 581 | OutputError(eris.Wrap(err, "request failed")) 582 | } 583 | 584 | if !resp.IsSuccess() { 585 | OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) 586 | } 587 | 588 | return resp.Payload.Accounts, nil 589 | } 590 | 591 | func getCurveForAddressFormat(addressFormat models.AddressFormat) *models.Curve { 592 | switch addressFormat { 593 | case models.AddressFormatEthereum, models.AddressFormatCosmos, models.AddressFormatUncompressed: 594 | return models.NewCurve(models.CurveSecp256k1) 595 | case models.AddressFormatSolana: 596 | return models.NewCurve(models.CurveEd25519) 597 | default: 598 | // we're here because either we haven't updated this switch statement after adding new 599 | // address formats OR we've hit an address format that supports multiple curves so we'll 600 | // make no assumptions on the expected curve 601 | return nil 602 | } 603 | } 604 | 605 | func getStandardPath(pathFormat models.PathFormat, addressFormat models.AddressFormat, accountIndex int) *string { 606 | // we currently only support BIP-32 so we'll make no assumptions about the path if given a different path format 607 | if pathFormat != models.PathFormatBip32 { 608 | return nil 609 | } 610 | 611 | var path string 612 | 613 | switch addressFormat { 614 | case models.AddressFormatEthereum: 615 | path = fmt.Sprintf(`m/44'/60'/%d'/0/0`, accountIndex) 616 | case models.AddressFormatCosmos: 617 | path = fmt.Sprintf(`m/44'/118'/%d'/0/0`, accountIndex) 618 | case models.AddressFormatSolana: 619 | path = fmt.Sprintf(`m/44'/501'/%d'/0'`, accountIndex) 620 | case models.AddressFormatUncompressed, models.AddressFormatCompressed: 621 | path = fmt.Sprintf(`m/%d'`, accountIndex) 622 | default: 623 | // we're here because we haven't updated this switch statement after adding new 624 | // address formats so we'll make no assumptions on the expected path 625 | return nil 626 | } 627 | 628 | return &path 629 | } 630 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tkhq/tkcli/src 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.0 6 | 7 | require ( 8 | github.com/btcsuite/btcutil v1.0.2 9 | github.com/google/uuid v1.3.1 10 | github.com/rotisserie/eris v0.5.4 11 | github.com/spf13/cobra v1.7.0 12 | github.com/stretchr/testify v1.8.4 13 | github.com/tkhq/go-sdk v0.0.0-20240813203011-ed45fe0d5c27 14 | github.com/tkhq/go-sdk/pkg/enclave_encrypt v0.0.0-20240513225018-5ebfb539ec1e 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 20 | github.com/cloudflare/circl v1.3.7 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 23 | github.com/ethereum/go-ethereum v1.14.5 // indirect 24 | github.com/go-logr/logr v1.2.4 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/go-openapi/analysis v0.21.4 // indirect 27 | github.com/go-openapi/errors v0.20.4 // indirect 28 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/loads v0.21.2 // indirect 31 | github.com/go-openapi/runtime v0.26.0 // indirect 32 | github.com/go-openapi/spec v0.20.9 // indirect 33 | github.com/go-openapi/strfmt v0.21.7 // indirect 34 | github.com/go-openapi/swag v0.22.4 // indirect 35 | github.com/go-openapi/validate v0.22.1 // indirect 36 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 37 | github.com/josharian/intern v1.0.0 // indirect 38 | github.com/mailru/easyjson v0.7.7 // indirect 39 | github.com/mitchellh/mapstructure v1.5.0 // indirect 40 | github.com/oklog/ulid v1.3.1 // indirect 41 | github.com/opentracing/opentracing-go v1.2.0 // indirect 42 | github.com/pkg/errors v0.9.1 // indirect 43 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 44 | github.com/spf13/pflag v1.0.5 // indirect 45 | go.mongodb.org/mongo-driver v1.12.1 // indirect 46 | go.opentelemetry.io/otel v1.19.0 // indirect 47 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 48 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 49 | golang.org/x/crypto v0.22.0 // indirect 50 | golang.org/x/sys v0.20.0 // indirect 51 | gopkg.in/yaml.v2 v2.4.0 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 3 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 4 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 5 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 6 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 7 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 8 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 9 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 10 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 11 | github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= 12 | github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 13 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 14 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 15 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 16 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 17 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 18 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 19 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 22 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 29 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 30 | github.com/ethereum/go-ethereum v1.14.5 h1:szuFzO1MhJmweXjoM5nSAeDvjNUH3vIQoMzzQnfvjpw= 31 | github.com/ethereum/go-ethereum v1.14.5/go.mod h1:VEDGGhSxY7IEjn98hJRFXl/uFvpRgbIIf2PpXiyGGgc= 32 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 33 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 34 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 35 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 36 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 37 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 38 | github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= 39 | github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= 40 | github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= 41 | github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 42 | github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 43 | github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 44 | github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= 45 | github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= 46 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 47 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 48 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 49 | github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= 50 | github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= 51 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 52 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 53 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 54 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 55 | github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= 56 | github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= 57 | github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= 58 | github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= 59 | github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= 60 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 61 | github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 62 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 63 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 64 | github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= 65 | github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= 66 | github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= 67 | github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= 68 | github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= 69 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 70 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 71 | github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 72 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 73 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 74 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 75 | github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= 76 | github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= 77 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 78 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 79 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 80 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 81 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 82 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 83 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 84 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 85 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 86 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 87 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 88 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 89 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 90 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 91 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 92 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 93 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 94 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 95 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 96 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 97 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 98 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 99 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 100 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 101 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 102 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 104 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 106 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 107 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 108 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 109 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 110 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 111 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 112 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 113 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 114 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 115 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 116 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 117 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 118 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 119 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 120 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 121 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 122 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 123 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 124 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 125 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 126 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 127 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 128 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 129 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 130 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 131 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 132 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 133 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 134 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 135 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 136 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 137 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 138 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 139 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 140 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 141 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 142 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 143 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 144 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 145 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 146 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 147 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 148 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 149 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 150 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 151 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 152 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 153 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 154 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 155 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 156 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 157 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 158 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 159 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 160 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 161 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 162 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 163 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 164 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 165 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 166 | github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= 167 | github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= 168 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 169 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 170 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 171 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 172 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 173 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 174 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 175 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 176 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 177 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 178 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 179 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 180 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 181 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 182 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 183 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 184 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 185 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 186 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 187 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 188 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 189 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 190 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 191 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 192 | github.com/tkhq/go-sdk v0.0.0-20240813182504-228a50933080 h1:Yhc2J2GCB0SDbLBVwK1ZlrYNiHVuwHGCU+N9CdJz4WQ= 193 | github.com/tkhq/go-sdk v0.0.0-20240813182504-228a50933080/go.mod h1:NgCPbnpGdhx+31NLwmK3iC6UftT7I70dbKXVbblVpjk= 194 | github.com/tkhq/go-sdk v0.0.0-20240813203011-ed45fe0d5c27 h1:1Tm6Z2uD9THuycnXtkNbTMf07Owdm071fV5JcKLsAQE= 195 | github.com/tkhq/go-sdk v0.0.0-20240813203011-ed45fe0d5c27/go.mod h1:2372WQ2x5SWlXmFBygP8PaNcR225Pn8Nd2WmzT9E35Y= 196 | github.com/tkhq/go-sdk/pkg/enclave_encrypt v0.0.0-20240513225018-5ebfb539ec1e h1:6TQn08QGF615Bt2LRNv1MwlI5qL9NlpO2A/DIKX8MUo= 197 | github.com/tkhq/go-sdk/pkg/enclave_encrypt v0.0.0-20240513225018-5ebfb539ec1e/go.mod h1:BvoxNhFz61TSwjbULvHYdeV0aS68qkcHXpGkJFVkzrw= 198 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 199 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 200 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 201 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 202 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 203 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 204 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 205 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 206 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 207 | go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= 208 | go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= 209 | go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= 210 | go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= 211 | go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= 212 | go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= 213 | go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= 214 | go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= 215 | go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= 216 | go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= 217 | go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= 218 | go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= 219 | go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= 220 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 221 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 222 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 223 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 224 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 225 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 226 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 227 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 228 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 229 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 230 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 231 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 235 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 236 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 237 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 238 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 239 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 258 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 259 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 260 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 261 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 262 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 263 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 264 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 265 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 266 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 267 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 268 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 269 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 270 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 271 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 272 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 273 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 274 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 275 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 276 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 277 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 278 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 279 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 280 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 281 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 282 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 283 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 284 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 285 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 286 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 287 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 288 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 289 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 290 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 291 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 292 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 293 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 294 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 295 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 296 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 297 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 298 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 299 | --------------------------------------------------------------------------------