├── .github ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml └── workflows │ ├── actionlint.yml │ ├── ci.yml │ ├── code-scan-cron.yml │ ├── dependabot-auto-merge.yml │ ├── release.yml │ └── triage.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── attest.go ├── certificate.go ├── create.go ├── decrypt.go ├── encrypt.go ├── fips.go ├── key.go ├── root.go ├── sign.go └── version.go ├── completions ├── bash_completion ├── fish_completion ├── powershell_completion └── zsh_completion ├── docker ├── Dockerfile ├── Dockerfile.cloud ├── Dockerfile.debian ├── Dockerfile.wolfi └── build │ └── entrypoint.sh ├── go.mod ├── go.sum ├── internal ├── flagutil │ └── flagutil.go └── termutil │ └── tui.go ├── main.go └── scripts ├── package-repo-import.sh └── package-upload.sh /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 6 | #### Name of feature: 7 | 8 | #### Pain or issue this feature alleviates: 9 | 10 | #### Why is this important to the project (if not answered above): 11 | 12 | #### Is there documentation on how to use this feature? If so, where? 13 | 14 | #### In what environments or workflows is this feature supported? 15 | 16 | #### In what environments or workflows is this feature explicitly NOT supported (if any)? 17 | 18 | #### Supporting links/other PRs/issues: 19 | 20 | 💔Thank you! 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions workflows 2 | on: 3 | push: 4 | workflow_call: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | actionlint: 16 | uses: smallstep/workflows/.github/workflows/actionlint.yml@main 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - 'v*' 7 | branches: 8 | - "master" 9 | pull_request: 10 | workflow_call: 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | ci: 18 | uses: smallstep/workflows/.github/workflows/goCI.yml@main 19 | with: 20 | only-latest-golang: false 21 | os-dependencies: "libpcsclite-dev" 22 | run-codeql: true 23 | secrets: inherit 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/code-scan-cron.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: '0 0 * * SUN' 4 | 5 | jobs: 6 | code-scan: 7 | uses: smallstep/workflows/.github/workflows/code-scan.yml@main 8 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot-auto-merge: 10 | uses: smallstep/workflows/.github/workflows/dependabot-auto-merge.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release & Upload Assets 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | ci: 11 | uses: smallstep/step-kms-plugin/.github/workflows/ci.yml@main 12 | secrets: inherit 13 | 14 | create_release: 15 | name: Create Release 16 | needs: ci 17 | runs-on: ubuntu-latest 18 | env: 19 | DOCKER_IMAGE: smallstep/step-kms-plugin 20 | CLOUD_TAG: cloud 21 | DEBIAN_TAG: bookworm 22 | WOLFI_TAG: wolfi 23 | outputs: 24 | version: ${{ steps.extract-tag.outputs.VERSION }} 25 | is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} 26 | docker_tags: ${{ env.DOCKER_TAGS }} 27 | docker_tags_cloud: ${{ env.DOCKER_TAGS_CLOUD }} 28 | docker_tags_debian: ${{ env.DOCKER_TAGS_DEBIAN }} 29 | docker_tags_wolfi: ${{ env.DOCKER_TAGS_WOLFI }} 30 | steps: 31 | - name: Is Pre-release 32 | id: is_prerelease 33 | run: | 34 | set +e 35 | echo ${{ github.ref }} | grep "\-rc.*" 36 | OUT=$? 37 | if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi 38 | echo "IS_PRERELEASE=${IS_PRERELEASE}" >> "${GITHUB_OUTPUT}" 39 | echo "IS_PRERELEASE=${IS_PRERELEASE}" >> "${GITHUB_ENV}" 40 | 41 | - name: Extract Tag Names 42 | id: extract-tag 43 | run: | 44 | VERSION=${GITHUB_REF#refs/tags/v} 45 | echo "VERSION=${VERSION}" >> "${GITHUB_OUTPUT}" 46 | # shellcheck disable=SC2129 47 | echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> "${GITHUB_ENV}" 48 | echo "DOCKER_TAGS_CLOUD=${{ env.DOCKER_IMAGE }}:${VERSION}-${CLOUD_TAG}" >> "${GITHUB_ENV}" 49 | echo "DOCKER_TAGS_DEBIAN=${{ env.DOCKER_IMAGE }}:${VERSION}-${DEBIAN_TAG}" >> "${GITHUB_ENV}" 50 | echo "DOCKER_TAGS_WOLFI=${{ env.DOCKER_IMAGE }}:${VERSION}-${WOLFI_TAG}" >> "${GITHUB_ENV}" 51 | 52 | - name: Add Latest Tag 53 | if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false' 54 | run: | 55 | # shellcheck disable=SC2129 56 | echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest" >> "${GITHUB_ENV}" 57 | echo "DOCKER_TAGS_CLOUD=${{ env.DOCKER_IMAGE }}:${CLOUD_TAG}" >> "${GITHUB_ENV}" 58 | echo "DOCKER_TAGS_DEBIAN=${{ env.DOCKER_IMAGE }}:${DEBIAN_TAG}" >> "${GITHUB_ENV}" 59 | echo "DOCKER_TAGS_WOLFI=${{ env.DOCKER_IMAGE }}:${WOLFI_TAG}" >> "${GITHUB_ENV}" 60 | 61 | - name: Create Release 62 | id: create_release 63 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | with: 67 | tag_name: ${{ github.ref_name }} 68 | name: Release ${{ github.ref_name }} 69 | draft: false 70 | prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} 71 | 72 | goreleaser: 73 | name: Upload Assets to Github w/ goreleaser 74 | runs-on: ubuntu-latest 75 | needs: create_release 76 | permissions: 77 | id-token: write 78 | contents: write 79 | packages: write 80 | env: 81 | GPG_PRIVATE_KEY_FILE: "0x889B19391F774443-Certify.key" 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: setup release environment 89 | run: |- 90 | # shellcheck disable=SC2129 91 | echo 'GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}' > .release-env 92 | { echo 'GORELEASER_KEY=${{ secrets.GORELEASER_KEY }}'; \ 93 | echo 'AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}'; \ 94 | echo 'AWS_S3_BUCKET=${{ secrets.AWS_S3_BUCKET }}'; \ 95 | echo 'AWS_S3_REGION=${{ secrets.AWS_S3_REGION }}'; \ 96 | echo 'AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}'; \ 97 | echo 'GPG_PRIVATE_KEY_FILE=${{ env.GPG_PRIVATE_KEY_FILE }}'; \ 98 | echo 'NFPM_PASSPHRASE=${{ secrets.GPG_PRIVATE_KEY_PASSWORD }}'; } >> .release-env 99 | 100 | - name: Write GPG private key to file 101 | run: | 102 | echo "${GPG_PRIVATE_KEY}" > "${GPG_PRIVATE_KEY_FILE}" 103 | shell: bash 104 | env: 105 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 106 | 107 | - name: Build binaries 108 | run: make release 109 | 110 | - name: Authenticate to Google Cloud 111 | if: ${{ needs.create_release.outputs.is_prerelease == 'false' }} 112 | id: gcloud-auth 113 | uses: google-github-actions/auth@v2 114 | with: 115 | token_format: access_token 116 | workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }} 117 | service_account: ${{ secrets.GOOGLE_CLOUD_GITHUB_SERVICE_ACCOUNT }} 118 | 119 | - name: Set up Google Cloud SDK 120 | if: ${{ needs.create_release.outputs.is_prerelease == 'false' }} 121 | uses: google-github-actions/setup-gcloud@v2 122 | with: 123 | project_id: ${{ secrets.GOOGLE_CLOUD_PACKAGES_PROJECT_ID }} 124 | 125 | - name: Get Release Date 126 | id: release_date 127 | run: | 128 | # shellcheck disable=SC2129 129 | RELEASE_DATE=$(date -u +"%y-%m-%d") 130 | echo "RELEASE_DATE=${RELEASE_DATE}" >> "${GITHUB_ENV}" 131 | echo 'IS_PRERELEASE=${{ needs.create_release.outputs.is_prerelease }}' >> "${GITHUB_ENV}" 132 | 133 | - name: Run GoReleaser Pro 134 | uses: goreleaser/goreleaser-action@v6.3.0 135 | with: 136 | distribution: goreleaser-pro 137 | version: v2.8.1 138 | args: publish 139 | env: 140 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 141 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 142 | AWS_S3_REGION: ${{ secrets.AWS_S3_REGION }} 143 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 144 | GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} 145 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 146 | NFPM_PASSPHRASE: ${{ secrets.GPG_PRIVATE_KEY_PASSWORD }} 147 | RELEASE_DATE: ${{ env.RELEASE_DATE }} 148 | IS_PRERELEASE: ${{ needs.create_release.outputs.is_prerelease }} 149 | 150 | - name: Shred and remove GPG private key 151 | run: | 152 | shred -zun 3 "${GPG_PRIVATE_KEY_FILE}" 153 | shred -zun 3 .release-env 154 | shell: bash 155 | 156 | build_upload_docker: 157 | name: Build & Upload Docker Image 158 | needs: create_release 159 | permissions: 160 | id-token: write 161 | contents: write 162 | uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main 163 | with: 164 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm 165 | tags: ${{ needs.create_release.outputs.docker_tags }} 166 | docker_image: smallstep/step-kms-plugin 167 | docker_file: docker/Dockerfile 168 | secrets: inherit 169 | 170 | build_upload_docker_debian: 171 | name: Build & Upload Debian Docker Image 172 | needs: create_release 173 | permissions: 174 | id-token: write 175 | contents: write 176 | uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main 177 | with: 178 | platforms: linux/amd64,linux/386,linux/arm,linux/arm64 179 | tags: ${{ needs.create_release.outputs.docker_tags_debian }} 180 | docker_image: smallstep/step-kms-plugin 181 | docker_file: docker/Dockerfile.debian 182 | secrets: inherit 183 | 184 | build_upload_docker_cloud: 185 | name: Build & Upload Cloud-Only Docker Image 186 | needs: create_release 187 | permissions: 188 | id-token: write 189 | contents: write 190 | uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main 191 | with: 192 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm 193 | tags: ${{ needs.create_release.outputs.docker_tags_cloud }} 194 | docker_image: smallstep/step-kms-plugin 195 | docker_file: docker/Dockerfile.cloud 196 | secrets: inherit 197 | 198 | build_upload_docker_wolfi: 199 | name: Build & Upload Wolfi Docker Image 200 | needs: create_release 201 | permissions: 202 | id-token: write 203 | contents: write 204 | uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main 205 | with: 206 | platforms: linux/amd64 207 | tags: ${{ needs.create_release.outputs.docker_tags_wolfi }} 208 | docker_image: smallstep/step-kms-plugin 209 | docker_file: docker/Dockerfile.wolfi 210 | secrets: inherit 211 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Add Issues and PRs to Triage 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | triage: 15 | uses: smallstep/workflows/.github/workflows/triage.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Go Workspaces 9 | go.work 10 | go.work.sum 11 | 12 | # Development 13 | bin/ 14 | .vscode/ 15 | 16 | # Release 17 | .release-env 18 | dist/ 19 | 20 | # Test binary, built with `go test -c` 21 | *.test 22 | 23 | # Output of the go coverage tool, specifically when used with LiteIDE 24 | *.out 25 | 26 | # Dependency directories (remove the comment below to include it) 27 | # vendor/ 28 | 29 | # Packages files 30 | 0x889B19391F774443-Certify.key 31 | gha-creds-*.json 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # step-kms-plugin .goreleaser.yml file. 2 | # Check the documentation at: 3 | # - http://goreleaser.com 4 | # - https://github.com/goreleaser/goreleaser-cross 5 | # - https://github.com/goreleaser/goreleaser-cross-example 6 | project_name: step-kms-plugin 7 | version: 2 8 | 9 | variables: 10 | packageName: step-kms-plugin 11 | packageRelease: 1 # Manually update release: in the nfpm section to match this value if you change this 12 | 13 | after: 14 | hooks: 15 | # This script depends on IS_PRERELEASE env being set. This is set by CI in the Is Pre-release step. 16 | - cmd: bash scripts/package-repo-import.sh {{ .Var.packageName }} {{ .Version }} 17 | output: true 18 | env: 19 | - IS_PRERELEASE={{ .Env.IS_PRERELEASE }} 20 | 21 | builds: 22 | - id: linux-amd64 23 | main: ./main.go 24 | binary: step-kms-plugin 25 | goos: 26 | - linux 27 | goarch: 28 | - amd64 29 | env: 30 | - CC=gcc 31 | - CXX=g++ 32 | flags: 33 | - -trimpath 34 | ldflags: 35 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 36 | - id: linux-arm64 37 | main: ./main.go 38 | binary: step-kms-plugin 39 | goos: 40 | - linux 41 | goarch: 42 | - arm64 43 | env: 44 | - CC=aarch64-linux-gnu-gcc 45 | - CXX=aarch64-linux-gnu-g++ 46 | flags: 47 | - -trimpath 48 | ldflags: 49 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 50 | - id: darwin-amd64 51 | main: ./main.go 52 | binary: step-kms-plugin 53 | goos: 54 | - darwin 55 | goarch: 56 | - amd64 57 | env: 58 | - CC=o64-clang 59 | - CXX=o64-clang++ 60 | - LD_LIBRARY_PATH=/osxcross/lib 61 | flags: 62 | - -trimpath 63 | ldflags: 64 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 65 | - id: darwin-arm64 66 | main: ./main.go 67 | binary: step-kms-plugin 68 | goos: 69 | - darwin 70 | goarch: 71 | - arm64 72 | env: 73 | - CC=oa64-clang 74 | - CXX=oa64-clang++ 75 | - LD_LIBRARY_PATH=/osxcross/lib 76 | flags: 77 | - -trimpath 78 | ldflags: 79 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 80 | - id: windows-amd64 81 | main: ./main.go 82 | binary: step-kms-plugin 83 | goos: 84 | - windows 85 | goarch: 86 | - amd64 87 | env: 88 | - CC=x86_64-w64-mingw32-gcc 89 | - CXX=x86_64-w64-mingw32-g++ 90 | flags: 91 | - -trimpath 92 | ldflags: 93 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 94 | - id: windows-arm64 95 | main: ./main.go 96 | binary: step-kms-plugin 97 | goos: 98 | - windows 99 | goarch: 100 | - arm64 101 | env: 102 | - CC=/llvm-mingw/bin/aarch64-w64-mingw32-gcc 103 | - CXX=/llvm-mingw/bin/aarch64-w64-mingw32-g++ 104 | flags: 105 | - -trimpath 106 | ldflags: 107 | - -s -w -X {{.ModulePath}}/cmd.Version={{.Version}} -X {{.ModulePath}}/cmd.ReleaseDate={{.Date}} 108 | 109 | archives: 110 | - id: step-kms-plugin 111 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" 112 | format_overrides: 113 | - goos: windows 114 | formats: [ 'zip' ] 115 | builds: 116 | - linux-amd64 117 | - linux-arm64 118 | - darwin-amd64 119 | - darwin-arm64 120 | - windows-amd64 121 | - windows-arm64 122 | wrap_in_directory: "{{ .ProjectName }}_{{ .Version }}" 123 | files: 124 | - README.md 125 | - LICENSE 126 | - completions/* 127 | 128 | nfpms: 129 | - id: packages 130 | builds: 131 | - linux-amd64 132 | - linux-arm64 133 | package_name: "{{ .Var.packageName }}" 134 | release: "1" 135 | file_name_template: >- 136 | {{- trimsuffix .ConventionalFileName .ConventionalExtension -}} 137 | {{- if and (eq .Arm "6") (eq .ConventionalExtension ".deb") }}6{{ end -}} 138 | {{- if not (eq .Amd64 "v1")}}{{ .Amd64 }}{{ end -}} 139 | {{- .ConventionalExtension -}} 140 | vendor: Smallstep Labs 141 | homepage: https://github.com/smallstep/step-kms-plugin 142 | maintainer: Smallstep 143 | description: > 144 | step-kms-plugin is a plugin for step to manage keys and certificates on a cloud KMSs and HSMs 145 | license: Apache 2.0 146 | section: utils 147 | formats: 148 | - deb 149 | - rpm 150 | priority: optional 151 | bindir: /usr/bin 152 | contents: 153 | - src: completions/bash_completion 154 | dst: /usr/share/bash-completion/completions/step-kms-plugin 155 | - src: completions/zsh_completion 156 | dst: /usr/share/zsh/vendor-completions/_step-kms-plugin 157 | packager: deb 158 | - src: completions/zsh_completion 159 | dst: /usr/share/zsh/site-functions/_step-kms-plugin 160 | packager: rpm 161 | rpm: 162 | signature: 163 | key_file: "{{ .Env.GPG_PRIVATE_KEY_FILE }}" 164 | deb: 165 | signature: 166 | key_file: "{{ .Env.GPG_PRIVATE_KEY_FILE }}" 167 | type: origin 168 | overrides: 169 | deb: 170 | dependencies: 171 | - libpcsclite1 172 | rpm: 173 | dependencies: 174 | - pcsc-lite-libs 175 | 176 | sboms: 177 | - id: archive 178 | artifacts: archive 179 | args: ["$artifact", "--output", "cyclonedx-json=$document"] 180 | env: 181 | - SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES=true 182 | - id: binary 183 | artifacts: binary 184 | args: ["$artifact", "--output", "cyclonedx-json=$document"] 185 | 186 | checksum: 187 | name_template: "checksums.txt" 188 | 189 | publishers: 190 | - name: Google Cloud Artifact Registry 191 | ids: 192 | - packages 193 | cmd: ./scripts/package-upload.sh {{ abs .ArtifactPath }} {{ .Var.packageName }} {{ .Version }} {{ .Var.packageRelease }} 194 | env: 195 | - IS_PRERELEASE={{ .Env.IS_PRERELEASE }} 196 | 197 | snapshot: 198 | version_template: "{{ .Tag }}" 199 | 200 | changelog: 201 | sort: asc 202 | 203 | release: 204 | github: 205 | owner: smallstep 206 | name: step-kms-plugin 207 | prerelease: auto 208 | draft: false 209 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @maraino @smallstep/core 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Smallstep Labs, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG?=github.com/smallstep/step-kms-plugin 2 | BINNAME?=step-kms-plugin 3 | GOLANG_CROSS_VERSION?=v1.24 4 | 5 | # Set V to 1 for verbose output from the Makefile 6 | Q=$(if $V,,@) 7 | PREFIX?= 8 | SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*") 9 | GOOS_OVERRIDE ?= 10 | OUTPUT_ROOT=output/ 11 | RELEASE=./.releases 12 | 13 | DOCKER_HOST ?= /var/run/docker.sock 14 | DOCKER_SOCK := $(if $(filter unix://%,$(DOCKER_HOST)),$(patsubst unix://%,%,$(DOCKER_HOST)),$(DOCKER_HOST)) 15 | 16 | ######################################### 17 | # Default 18 | ######################################### 19 | 20 | all: lint test build 21 | 22 | ci: test build 23 | 24 | .PHONY: all ci 25 | 26 | ######################################### 27 | # Bootstrapping 28 | ######################################### 29 | 30 | bootstrap: 31 | $Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest 32 | $Q go install golang.org/x/vuln/cmd/govulncheck@latest 33 | $Q go install gotest.tools/gotestsum@latest 34 | 35 | .PHONY: bootstrap 36 | 37 | ################################################# 38 | # Determine the type of `push` and `version` 39 | ################################################# 40 | 41 | # GITHUB Actions 42 | ifdef GITHUB_REF 43 | VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///') 44 | NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc) 45 | else 46 | VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev") 47 | endif 48 | 49 | VERSION := $(shell echo $(VERSION) | sed 's/^v//') 50 | DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC') 51 | 52 | ifdef V 53 | $(info GITHUB_REF is $(GITHUB_REF)) 54 | $(info VERSION is $(VERSION)) 55 | $(info DATE is $(DATE)) 56 | endif 57 | 58 | ######################################### 59 | # Build 60 | ######################################### 61 | 62 | LDFLAGS := -ldflags='-s -w -X "$(PKG)/cmd.Version=$(VERSION)" -X "$(PKG)/cmd.ReleaseDate=$(DATE)"' 63 | 64 | build: 65 | $Q go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) 66 | @echo "Build Complete!" 67 | 68 | build-fips: 69 | $Q GOEXPERIMENT="boringcrypto" go build -v -tags fips,noyubikey -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) 70 | @echo "Build Complete!" 71 | 72 | .PHONY: build build-fips 73 | 74 | ######################################### 75 | # Go generate 76 | ######################################### 77 | 78 | generate: build 79 | $Q go generate ./... 80 | $Q mkdir -p completions 81 | $Q bin/step-kms-plugin completion bash > completions/bash_completion 82 | $Q bin/step-kms-plugin completion fish > completions/fish_completion 83 | $Q bin/step-kms-plugin completion powershell > completions/powershell_completion 84 | $Q bin/step-kms-plugin completion zsh > completions/zsh_completion 85 | 86 | .PHONY: generate 87 | 88 | ######################################### 89 | # Test 90 | ######################################### 91 | 92 | test: 93 | $Q go test -coverprofile=coverage.out ./... 94 | 95 | .PHONY: test 96 | 97 | ######################################### 98 | # Linting 99 | ######################################### 100 | 101 | fmt: 102 | $Q goimports --local github.com/smallstep/step-kms-plugin -l -w $(SRC) 103 | 104 | lint: golint govulncheck 105 | 106 | golint: SHELL:=/bin/bash 107 | golint: 108 | $Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m 109 | 110 | govulncheck: 111 | $Q govulncheck ./... 112 | 113 | .PHONY: fmt lint golint govulncheck 114 | 115 | ######################################### 116 | # Release 117 | ######################################### 118 | 119 | release-dev: 120 | $Q @docker run -it --rm --privileged -e CGO_ENABLED=1 \ 121 | -e GORELEASER_KEY=$(GORELEASER_KEY) \ 122 | -e IS_PRERELEASE=true \ 123 | --entrypoint /bin/bash \ 124 | -v $(DOCKER_SOCK):/var/run/docker.sock:Z \ 125 | -v `pwd`:/go/src/$(PKG):Z \ 126 | -w /go/src/$(PKG) \ 127 | ghcr.io/goreleaser/goreleaser-cross-pro:${GOLANG_CROSS_VERSION} 128 | 129 | release-dry-run: 130 | $Q @docker run --rm --privileged -e CGO_ENABLED=1 \ 131 | -e GORELEASER_KEY=$(GORELEASER_KEY) \ 132 | -e GPG_PRIVATE_KEY_FILE=/dev/null \ 133 | -e IS_PRERELEASE=true \ 134 | --entrypoint /go/src/$(PKG)/docker/build/entrypoint.sh \ 135 | -v $(DOCKER_SOCK):/var/run/docker.sock:Z \ 136 | -v `pwd`:/go/src/$(PKG):Z \ 137 | -w /go/src/$(PKG) \ 138 | ghcr.io/goreleaser/goreleaser-cross-pro:${GOLANG_CROSS_VERSION} \ 139 | release --clean --skip=validate --skip=sign --prepare 140 | 141 | release: 142 | @if [ ! -f ".release-env" ]; then \ 143 | echo "\033[91m.release-env is required for release\033[0m";\ 144 | exit 1;\ 145 | fi 146 | $Q @docker run --rm --privileged -e CGO_ENABLED=1 --env-file .release-env \ 147 | --entrypoint /go/src/$(PKG)/docker/build/entrypoint.sh \ 148 | -v ./$(GPG_PRIVATE_KEY_FILE):/$(GPG_PRIVATE_KEY_FILE) \ 149 | -v $(DOCKER_SOCK):/var/run/docker.sock \ 150 | -v `pwd`:/go/src/$(PKG) \ 151 | -w /go/src/$(PKG) \ 152 | ghcr.io/goreleaser/goreleaser-cross-pro:${GOLANG_CROSS_VERSION} \ 153 | release --clean --prepare 154 | 155 | .PHONY: release-dev release-dry-run release 156 | 157 | ######################################### 158 | # Install 159 | ######################################### 160 | 161 | INSTALL_PREFIX?=/usr/ 162 | 163 | install: $(PREFIX)bin/$(BINNAME) 164 | $Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME) 165 | 166 | uninstall: 167 | $Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME) 168 | 169 | .PHONY: install uninstall 170 | 171 | ######################################### 172 | # Clean 173 | ######################################### 174 | 175 | clean: 176 | ifneq ($(BINNAME),"") 177 | $Q rm -f bin/$(BINNAME) 178 | endif 179 | 180 | .PHONY: clean 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # step-kms-plugin 2 | 3 | This is a tool that helps manage keys and certificates on a cloud KMSs and HSMs. 4 | It can be used independently, or as a plugin for [`step`](https://github.com/smallstep/cli). 5 | 6 | > ⚠️ This tool is currently in beta mode and its usage of might change without 7 | > announcements. 8 | 9 | ## Installation 10 | 11 | There's two installation options: 12 | 13 | - The most generic way to install `step-kms-plugin` is to use `go install` to 14 | compile it and install it in your `$GOBIN`, which defaults to `$(go env GOPATH)/bin`. 15 | 16 | ```console 17 | go install github.com/smallstep/step-kms-plugin@latest 18 | ``` 19 | 20 | - Alternatively, download the [latest release binary for your platform](https://github.com/smallstep/step-kms-plugin/releases). 21 | 22 | To use `step-kms-plugin` as a plugin for `step` (eg. `step kms create ...`), 23 | add it to your `$PATH` or to `$(step path --base)/plugins`. 24 | 25 | ## Supported KMSs 26 | 27 | The following Key Management Systems (KMSs) are supported, but not all of 28 | them provide the full functionality: 29 | 30 | * PKCS #11 modules 31 | * [TPM 2.0](https://trustedcomputinggroup.org/resource/tpm-library-specification/) 32 | * [Amazon AWS KMS](https://aws.amazon.com/kms/) 33 | * [Google Cloud Key Management](https://cloud.google.com/security-key-management) 34 | * [Microsoft Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) 35 | * [YubiKey PIV](https://developers.yubico.com/PIV/) 36 | * ssh-agent 37 | 38 | ## Setting up `step-ca`? 39 | 40 | If you're setting up a `step-ca` PKI on one of the supported KMSs, check out our [detailed tutorials in our Cryptographic Protection docs](https://smallstep.com/docs/step-ca/cryptographic-protection/). 41 | 42 | ## Authenticating to a Cloud KMS provider 43 | 44 | If you use `step-kms-plugin` with a cloud provider, you will need to authenticate to the cloud provider. 45 | Here are the required API permissions and authentication methods for each provider. 46 | 47 | ### AWS KMS 48 | 49 | `step-kms-plugin` authenticates to AWS using the same approach as any AWS Go SDK program. 50 | See the [AWS Go SDK documentation](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) for details. 51 | 52 | The following IAM role Actions may be required by `step-kms-plugin`: 53 | 54 | ``` 55 | kms:GetPublicKey 56 | kms:CreateKey 57 | kms:CreateAlias 58 | kms:Sign 59 | ``` 60 | 61 | Notes: 62 | * [AWS Docs: IAM Policies relating to KMS keys](https://docs.aws.amazon.com/kms/latest/developerguide/cmks-in-iam-policies.html) 63 | 64 | ### Azure Key Vault 65 | 66 | Authentication to Azure is handled via environment variables; we recommend using either [file-based authentication](https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authorization#use-file-based-authentication) via the `AZURE_AUTH_LOCATION` environment variable, or using a managed identity and setting the `AZURE_TENANT_ID` and `AZURE_CLIENT_ID` variables when running `step-kms-plugin` 67 | 68 | Alternatively, you can create a [service principal](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli) and set the `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` variables. See Option 1 under [Authentication Methods for Azure SDK for Go](https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#-option-1-define-environment-variables) for examples of authentication methods and environment variables. 69 | 70 | For local development and testing, Azure CLI credentials are used if no authentication environment variables are set. 71 | 72 | These are the Azure RBAC role actions used by `step-kms-plugin`: 73 | 74 | ``` 75 | Key Vault Crypto Officer 76 | Key Vault Secrets Officer 77 | Key Vault Certificates Officer 78 | ``` 79 | 80 | Notes: 81 | * It is recommended to restrict permissions to the vaults that you will use `step-kms-plugin` with. 82 | * Azure has several built-in [RBAC roles for Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli#azure-built-in-roles-for-key-vault-data-plane-operations). 83 | 84 | ### Google Cloud KMS 85 | 86 | To authenticate, be sure you have installed [the `gcloud` CLI](https://cloud.google.com/sdk/docs/install) and have [configured Google Cloud application default credentials](https://developers.google.com/accounts/docs/application-default-credentials) in your local environment, eg. by running `gcloud auth application-default login`. 87 | 88 | The following Cloud KMS permissions may be required for `step-kms-plugin`: 89 | 90 | ``` 91 | cloudkms.cryptoKey.create 92 | cloudkms.cryptoKeyVersions.create 93 | cloudkms.cryptoKeyVersions.get 94 | cloudkms.cryptoKeyVersions.useToDecrypt 95 | cloudkms.cryptoKeyVersions.useToSign 96 | cloudkms.cryptoKeyVersions.useToVerify 97 | cloudkms.cryptoKeyVersions.viewPublicKey 98 | cloudkms.keyRings.get 99 | cloudkms.keyRings.create 100 | cloudkms.locations.get 101 | resourcemanager.projects.get 102 | ``` 103 | 104 | Notes: 105 | * It is recommended that you scope the IAM role permissions to specific key rings 106 | * When creating a key, if the key ring does not exist, `step-kms-plugin` will attempt to create it first. 107 | 108 | ## General Usage 109 | 110 | `step-kms-plugin` can be used as a standalone application or in conjunction with 111 | `step`. 112 | 113 | The commands under `step kms` will directly call `step-kms-plugin` with the 114 | given arguments. For example, these two commands are equivalent: 115 | 116 | ```console 117 | step kms create --kty EC --crv P384 'pkcs11:token=smallstep;id=1000;object=mykey?pin-source=/dev/shm/pass.txt' 118 | step-kms-plugin create --kty EC --crv P384 'pkcs11:token=smallstep;id=1000;object=mykey?pin-source=/dev/shm/pass.txt' 119 | ``` 120 | 121 | For the rest of the examples, we are going to use the plugin usage, `step kms`, 122 | using the PKCS #11 KMS with [SoftHSM 2](https://github.com/opendnssec/SoftHSMv2). 123 | To initialize the SoftHSM 2 we will run: 124 | 125 | ```console 126 | $ softhsm2-util --init-token --free --token smallstep \ 127 | --label smallstep --so-pin password --pin password 128 | Slot 0 has a free/uninitialized token. 129 | The token has been initialized and is reassigned to slot 715175552 130 | ``` 131 | 132 | You can later delete it running: 133 | 134 | ```console 135 | softhsm2-util --delete-token --token smallstep 136 | ``` 137 | 138 | ### p11-kit integration 139 | 140 | When `step-kms-plugin` is used with PKCS #11, it needs the filename of the PKCS 141 | #11 module to load. The module can be directly passed using the `module-path` 142 | parameter. But if it is not defined, it will use the 143 | [p11-kit-proxy](https://p11-glue.github.io/p11-glue/p11-kit/manual/sharing.html) 144 | module provided by the 145 | [p11-kit](https://p11-glue.github.io/p11-glue/p11-kit.html) project. 146 | 147 | p11-kit provides a standard configuration setup for installing PKCS #11 modules 148 | so they're discoverable. p11-kit uses a configuration file 149 | [pkcs11.conf](https://p11-glue.github.io/p11-glue/p11-kit/manual/pkcs11-conf.html) 150 | that allows you to define the module to use. By default, it also allows you to 151 | add configurations in the user's home directory. We will use this last option to 152 | define the SoftHSM 2 module. 153 | 154 | With a simple file like following in `~/.config/pkcs11/modules/softhsm2`, 155 | p11-kit-proxy will know which module to load: 156 | 157 | ``` 158 | module: /usr/local/lib/softhsm/libsofthsm2.so 159 | ``` 160 | 161 | This configuration will make these commands equivalent: 162 | 163 | ```console 164 | step-kms-plugin create --kty EC --crv P384 'pkcs11:token=smallstep;id=1000;object=mykey?pin-value=password' 165 | step-kms-plugin create --kty EC --crv P384 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep;id=1000;object=mykey?pin-value=password' 166 | ``` 167 | 168 | In the following sections, we will skip the `module-path` and assume that 169 | p11-kit is properly configured with SoftHSM 2. 170 | 171 | ### Creating a new key 172 | 173 | Our PKCS #11 implementation requires always an object id (`id=1000`) and label 174 | (`object=my-key`) to create the key. We will add those as part of the URI that 175 | defines the module to use. 176 | 177 | By default, the create command creates an EC P-256 key: 178 | 179 | ```console 180 | $ step kms create 'pkcs11:token=smallstep;id=1000;object=my-p256?pin-value=password' 181 | -----BEGIN PUBLIC KEY----- 182 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjg5Zs/fSuvfodhZQcxcu07deKsdX 183 | sQf46/JPxQ39kPIkhD+onVVxCl462yMGVTQeLDCN3fwImoOdqZ3eKhoQOA== 184 | -----END PUBLIC KEY----- 185 | ``` 186 | 187 | We can use `--kty`, `--crv`, and `--size` to create other types of keys. On 188 | other KMS implementations you can also use the `--pss` and `--alg` flags to 189 | define precisely the key to generate. Here we are creating a P-384 and a 190 | 3072-bit RSA key: 191 | 192 | ```console 193 | $ step kms create --kty EC --crv P384 'pkcs11:token=smallstep;id=1001;object=my-p384?pin-value=password' 194 | -----BEGIN PUBLIC KEY----- 195 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBDdy2wnC6r8n2qZTa3kefjo3CEkaWXz6 196 | rWTbDNEYrzc9LXEoA7zI1j+liSGR9wLmu91keOBnweQOIR06QV12InEKFX2l3lRx 197 | nDPvq7P3MeRo9UqzKlZT+D+dhYQjB54K 198 | -----END PUBLIC KEY----- 199 | $ step kms create --kty RSA --size 3072 'pkcs11:token=smallstep;id=1002;object=my-rsa?pin-value=password' 200 | -----BEGIN PUBLIC KEY----- 201 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzAtriAh4ABfboPff15CD 202 | Skzxghaeb5SqcCwvZYdlZDRlZHlcbweY80bHjFvcU+ytSZOoMgBw+XooUnTmeVo3 203 | hOy039wlZwkfv/MEL4HP1AUVNA2iS19tmybAUTK0Myl/Ui+1iGllYA3e1ChCEZwV 204 | 3B5KPfpG5KiPurhKfv5q3edIVcMKL8qj8Y9HYrzFBebQil23vkWrFylb1r/54W/O 205 | 5kT2emYEGaJ8lJqzvJaIsvQpk8EqkJ7FHuAMeZyb3BK8cGjIP/GI22mL6NO3LpFc 206 | PK3Zjo7mZS5tQlFR9CULEbCuM+jiOs7FRJdyhUhdkygDxuWk1hfrCMYcG59P8pQX 207 | mPaCwE78GB3Bsi50Bp4+UI9KBcp+JARdPKocd6RvASDX1KpALpFhgqrC05+JfKA+ 208 | /51QMYY1mlJn7izHmwYJr0DRn1usrh5mtJEcOtwiNKR3bo1LI00XW93DIA442IzA 209 | KqMBZrEYmuy+oL6Jy9Ys4nOVWEzFcOjmUWyjFOMMG/89AgMBAAE= 210 | -----END PUBLIC KEY----- 211 | ``` 212 | 213 | SoftHSM 2 does not support creating an extractable key, but on other devices, it 214 | is recommended to use the `--extractable` flag so you can backup a wrapped 215 | version of the new keys. 216 | 217 | ### Getting the public key 218 | 219 | To retrieve the public key, we can use the `id`, the `object`, or both at the 220 | same time: 221 | 222 | ```console 223 | $ step kms key 'pkcs11:token=smallstep;id=1000;object=my-p256?pin-value=password' 224 | -----BEGIN PUBLIC KEY----- 225 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjg5Zs/fSuvfodhZQcxcu07deKsdX 226 | sQf46/JPxQ39kPIkhD+onVVxCl462yMGVTQeLDCN3fwImoOdqZ3eKhoQOA== 227 | -----END PUBLIC KEY----- 228 | $ step kms key 'pkcs11:token=smallstep;id=1001?pin-value=password' 229 | -----BEGIN PUBLIC KEY----- 230 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBDdy2wnC6r8n2qZTa3kefjo3CEkaWXz6 231 | rWTbDNEYrzc9LXEoA7zI1j+liSGR9wLmu91keOBnweQOIR06QV12InEKFX2l3lRx 232 | nDPvq7P3MeRo9UqzKlZT+D+dhYQjB54K 233 | -----END PUBLIC KEY----- 234 | $ step kms key 'pkcs11:token=smallstep;object=my-rsa?pin-value=password' 235 | -----BEGIN PUBLIC KEY----- 236 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzAtriAh4ABfboPff15CD 237 | Skzxghaeb5SqcCwvZYdlZDRlZHlcbweY80bHjFvcU+ytSZOoMgBw+XooUnTmeVo3 238 | hOy039wlZwkfv/MEL4HP1AUVNA2iS19tmybAUTK0Myl/Ui+1iGllYA3e1ChCEZwV 239 | 3B5KPfpG5KiPurhKfv5q3edIVcMKL8qj8Y9HYrzFBebQil23vkWrFylb1r/54W/O 240 | 5kT2emYEGaJ8lJqzvJaIsvQpk8EqkJ7FHuAMeZyb3BK8cGjIP/GI22mL6NO3LpFc 241 | PK3Zjo7mZS5tQlFR9CULEbCuM+jiOs7FRJdyhUhdkygDxuWk1hfrCMYcG59P8pQX 242 | mPaCwE78GB3Bsi50Bp4+UI9KBcp+JARdPKocd6RvASDX1KpALpFhgqrC05+JfKA+ 243 | /51QMYY1mlJn7izHmwYJr0DRn1usrh5mtJEcOtwiNKR3bo1LI00XW93DIA442IzA 244 | KqMBZrEYmuy+oL6Jy9Ys4nOVWEzFcOjmUWyjFOMMG/89AgMBAAE= 245 | -----END PUBLIC KEY----- 246 | ``` 247 | 248 | ### Signing 249 | 250 | Let's first, initialize a hello world to sign: 251 | 252 | ```console 253 | echo "Hello World" > data.txt 254 | ``` 255 | 256 | And then use the previous keys to sign, as before we can either use `id`, 257 | `object`, or both: 258 | 259 | ```console 260 | $ step kms sign --in data.txt 'pkcs11:token=smallstep;id=1000;object=my-p256?pin-value=password' 261 | MEQCIH2WfsgVRfCJs/sgIftT3i7xbpslS+9ShW/3qO9jXeA7AiBZFkcum+68zQ7pxluUE1v1yjCDyo34OEGIIyic9ItBcA== 262 | $ step kms sign --in data.txt 'pkcs11:token=smallstep;id=1001?pin-value=password' 263 | MGUCMQDtK5cADG4D3AXRLeTLvOpcDoOfYHJqt8eVVhKPg+Q5z9Hk3DSBlz/h1+YGyV11crYCMEVnQIqSdYQLB096DyLrZG28+cMjKfs+mlg+UUeVShnjHgBNGt2tHCeAZAS0VV4u4A== 264 | $ step kms sign --in data.txt 'pkcs11:token=smallstep;object=my-rsa?pin-value=password' 265 | AYbuQf6JfQMxrnaolaOyaddW4dfHU4Rg/mXXYTzSns3WUxuxJ2yXysm2Af0DrSaoqg3J4pFAmKiadDf+AZeBi0Lwwx1GpTxxOaiGDAuCuUJyUDcA/G2mTNX9+eEQkI/vOIM6Z+5T9kqP48BN3GKV5e51feSmkP6ihnQVXhW7kgPDOWt2Qq3GjJvOrn0pIjSgiYMYviMDvgcgxPuIhktYc7ZBWW63gmZ40nR3TFzTveWn7vBCGPJMOi6eOjPKRvpzo0II5froUgbTZXXFfb0r7xhMx872i2/MlRL/xhc0iy2BEXWWcoJovrbO5SdMGM0iDDkAOYceQxqW+HPf6Ghd7KA/hP6Rr0PwpfdxW7h8fF45bHrKDCXzIY4U+tHF9E16adA5axDwHVSnO8Hm5tajhB0VM7w3DYnu3npX/ko4RJw/kXe0PzhBqr+f67mhoCOuKkrsc8p8ABensZA5LeWivo78i1KMFWkh9SMRcq728GUx5/wdkc9boYr/jFNJ4WKf 266 | ``` 267 | 268 | We can use `step` to verify one of these signatures: 269 | 270 | ```console 271 | $ step kms key 'pkcs11:token=smallstep;id=1000?pin-value=password' > my-p256.key 272 | $ step crypto key verify --key my-p256.key \ 273 | --signature MEQCIH2WfsgVRfCJs/sgIftT3i7xbpslS+9ShW/3qO9jXeA7AiBZFkcum+68zQ7pxluUE1v1yjCDyo34OEGIIyic9ItBcA== data.txt 274 | true 275 | ``` 276 | 277 | By default, RSA signatures use the RSA PKCS #1 scheme with SHA256, but we can 278 | use the `--alg` to select the hash function and `--pss` to use the RSA-PSS 279 | scheme: 280 | 281 | ```console 282 | $ step kms sign --alg SHA512 --in data.txt 'pkcs11:token=smallstep;object=my-rsa?pin-value=password' 283 | ljdurcImEwOgAOxntf4+U56w1+lf8V/wOWrRfMS3PvtSz3KSfRZZu10ZxtIG7ilZFNUnb0svTM9e3+ViYCOX+3zxu22F4DWp6E5S92TbS7AImQbMybl7rYtYloDBSagJ5T0d1h3wVg77Npi5Fkcn39ekWDeSmrEK359H0EJAoSFTVfYJ4vYUvFHbO5Zn/BgWQNtTVoSCDSnSX1cu3Nar9N2bAcGbbfjBcwQsjg+NDRgdxxNJESKYHL280gvZR0wLpYvX4jf57UUVLF4WdMEh1YGPsBGzO/M1rSMS8pYZVD1kNOwIq7buFGAVAgl8UirtIe0joUYQekVhFbGEgADTv33fWOa8B95ARGraR5mE0lg5vwC/8SeZL9M4iIS5cdn+lOs/Nj5GDySykpgsCPi+irqKRgMcC88omv8/ofqcUIJpm+IOhE47IvL3CjlItEJV55kQjC4qrdNb9/w4vk7fFtW/amdxb5juajU2AIfuKFduaHhwSJpyguzmi2Zc0r7e 284 | $ step kms sign --alg SHA256 --pss --in data.txt 'pkcs11:token=smallstep;object=my-rsa?pin-value=password' 285 | iT/enoe6zXfz4bZolrQUoYf+B5bDhn++cfkgM4x4ozqX8xd6lljPMODGB3Z43rfvUHc3A//ULzN8DjAzA7nuExneexrlAalwhqeMHSLHAmJsztgpuQ2OHdpsEfWIbkQgd4lfZWBq0Srri32SEqTnqp+s2Itf1R1By6PFcsftVMFvH3foXn3bEwWK8gHsxLRt/bnqC7ubXU+b/xjUQiu/LMl+p7RSFVjDtm3e0j07G6cbsABsr4EA0Xlw7JRrYbiP3hz4GwfRbfSBKBXrCF+edpBhGtscJnrdwL9LD/MbaDgEWrf8lO1UFmLp2B6NsjvNiQhZJ4ruQ4isHOF669z5cFcB5Hc14i4ZI81dYAI8AjG7NZvF07bH32gM2h6vVEgesrTqqcKpLW/dge3cpcEimA0Nfzpeg6ZnhnugCtI8FBDZAbo3KP9e4O2mXydP5MmZu4vWajjWc4h3sReBFXg888j2dh8gsJXCIGNUXUzULHysfdTVivnewtW2sDDnEK+L 286 | ``` 287 | 288 | ### Signing certificates with step 289 | 290 | The `step-kms-plugin` is automatically used by `step certificate create` and 291 | `step certificate sign` commands if we use the `--kms` flag. With these 292 | commands, we can initialize our PKI using a key stored in a KMS. 293 | 294 | Let's create first a root key: 295 | 296 | ```console 297 | $ step kms create 'pkcs11:token=smallstep;id=2000;object=root?pin-value=password' 298 | -----BEGIN PUBLIC KEY----- 299 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5fbczGkGeLrLu3nD7mbdS0PmqDUT 300 | jT/f0r5U71dCAhP2T4rTfdrgnFPacX/a4jeQ1sMn4grtNFc1A4CE6vBt0A== 301 | -----END PUBLIC KEY----- 302 | ``` 303 | 304 | And use it to create the root certificate: 305 | 306 | ```console 307 | $ step certificate create --profile root-ca \ 308 | --kms 'pkcs11:token=smallstep?pin-value=password' \ 309 | --key 'pkcs11:id=2000' \ 310 | "Smallstep Root CA" root_ca.crt 311 | Your certificate has been saved in root_ca.crt. 312 | ``` 313 | 314 | Note that currently, the configuration of the KMS and the reference to the key 315 | is passed using two different flags. This might be improved in the future. 316 | 317 | Now let's create a key for the intermediate certificate: 318 | 319 | ```console 320 | $ step kms create 'pkcs11:token=smallstep;id=2001;object=intermediate?pin-value=password' 321 | -----BEGIN PUBLIC KEY----- 322 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZUm9hNkXPn9KrXrG1vzhgzTwqD4+ 323 | j0Wo9CQOP7GQApJLcVO9TGpzpLQHEUsUEU2zAnrGlxH7oFAlbZGXH4ueHQ== 324 | -----END PUBLIC KEY----- 325 | ``` 326 | 327 | And create the intermediate ca: 328 | 329 | ```console 330 | $ step certificate create --profile intermediate-ca \ 331 | --kms 'pkcs11:token=smallstep?pin-value=password' \ 332 | --ca root_ca.crt --ca-key 'pkcs11:id=2000' \ 333 | --key pkcs11:id=2001 \ 334 | "Smallstep IntermediateCA" intermediate_ca.crt 335 | Your certificate has been saved in intermediate_ca.crt. 336 | ``` 337 | 338 | We can also create a CSR backed by a key in the KMS and sign it using the intermediate key: 339 | 340 | ```console 341 | $ step kms create 'pkcs11:token=smallstep;id=2002;object=leaf?pin-value=password' 342 | -----BEGIN PUBLIC KEY----- 343 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6fMBiDFPAOCrHSQszpoLMQX9JYuk 344 | JVX8J8X9t/OydimJAgBujwY8xRSgnWdU1SXXdMck+wPZZNBYvcWJWpLN9Q== 345 | -----END PUBLIC KEY----- 346 | $ step certificate create --csr \ 347 | --kms 'pkcs11:token=smallstep?pin-value=password' \ 348 | --key pkcs11:id=2002 \ 349 | leaf.internal leaf.csr 350 | Your certificate signing request has been saved in leaf.csr. 351 | $ step certificate sign --kms 'pkcs11:token=smallstep?pin-value=password' \ 352 | leaf.csr intermediate_ca.crt pkcs11:id=2001 353 | -----BEGIN CERTIFICATE----- 354 | MIIBxTCCAWygAwIBAgIQeauacIrgtv7uPgzk+Z4puzAKBggqhkjOPQQDAjAjMSEw 355 | HwYDVQQDExhTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlQ0EwHhcNMjIwNzEyMjE0MTI4 356 | WhcNMjIwNzEzMjE0MTI4WjAYMRYwFAYDVQQDEw1sZWFmLmludGVybmFsMFkwEwYH 357 | KoZIzj0CAQYIKoZIzj0DAQcDQgAE6fMBiDFPAOCrHSQszpoLMQX9JYukJVX8J8X9 358 | t/OydimJAgBujwY8xRSgnWdU1SXXdMck+wPZZNBYvcWJWpLN9aOBjDCBiTAOBgNV 359 | HQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1Ud 360 | DgQWBBRx1wnkugvbRewcTzIyBDwkeM2qVzAfBgNVHSMEGDAWgBRWvP5Nn9rbZ5Go 361 | 24uF0oUqmHEghjAYBgNVHREEETAPgg1sZWFmLmludGVybmFsMAoGCCqGSM49BAMC 362 | A0cAMEQCICWSdIWIStDm5OJqBlqo1fd4lpzkcM0AOQcCwer+AgO1AiAF3sK+26LI 363 | mX6QduO/H/k8GZzcx923U54bRPCxKUaPvg== 364 | -----END CERTIFICATE----- 365 | ``` 366 | 367 | -------------------------------------------------------------------------------- /cmd/attest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "crypto" 18 | "crypto/ecdsa" 19 | "crypto/ed25519" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "crypto/sha256" 24 | "crypto/x509" 25 | "encoding/base64" 26 | "encoding/pem" 27 | "errors" 28 | "fmt" 29 | "io" 30 | "os" 31 | 32 | "github.com/fxamacker/cbor/v2" 33 | "github.com/spf13/cobra" 34 | "go.step.sm/crypto/kms/apiv1" 35 | "go.step.sm/crypto/pemutil" 36 | 37 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 38 | ) 39 | 40 | // attestCmd represents the attest command 41 | var attestCmd = &cobra.Command{ 42 | Use: "attest ", 43 | Short: "create an attestation certificate", 44 | Long: `Print an attestation certificate, an endorsement key, or if the "--format" flag 45 | is set, an attestation object. Currently this command is only supported with 46 | YubiKeys and the TPM KMS. 47 | 48 | An attestation object can be used to resolve an ACME device-attest-01 challenge. 49 | To pass this challenge, the client needs proof of possession of a private key by 50 | signing the ACME key authorization. The format is defined in RFC 8555 as a 51 | string that concatenates the challenge token for the challenge with the ACME 52 | account key fingerprint separated by a "." character: 53 | 54 | keyAuthorization = token || '.' || base64url(Thumbprint(accountKey))`, 55 | Example: ` # Get the attestation certificate from a YubiKey: 56 | step-kms-plugin attest yubikey:slot-id=9c 57 | 58 | # Create an attestation object used in an ACME device-attest-01 flow: 59 | echo -n . | step-kms-plugin attest --format step yubikey:slot-id=9c 60 | 61 | # Get the attestation certificate belonging to an Attestion Key, using the default TPM KMS: 62 | step-kms-plugin attest 'tpmkms:name=my-ak;ak=true' 63 | 64 | # Get the attestation certificate chain for an attested key, using the default TPM KMS: 65 | step-kms-plugin attest tpmkms:name=my-attested-key 66 | 67 | # Get the attestation certificate for an attested key, using the default TPM KMS: 68 | step-kms-plugin attest --leaf tpmkms:name=my-attested-key 69 | 70 | # Create an attestation statement for an attested key, using the default TPM KMS: 71 | step-kms-plugin attest --format tpm tpmkms:name=my-attested-key 72 | 73 | # Create an attestation statement for an attested key, using the default TPM KMS, 74 | enrolling with a Smallstep Attestation CA if no AK certificate is available (yet): 75 | step-kms-plugin attest --format tpm 'tpmkms:name=my-attested-key;attestation-ca-url=https://my.attestation.ca/url;attestation-ca-root=/path/to/trusted/roots.pem'`, 76 | RunE: func(cmd *cobra.Command, args []string) error { 77 | if len(args) != 1 { 78 | return showErrUsage(cmd) 79 | } 80 | 81 | name := args[0] 82 | flags := cmd.Flags() 83 | format := flagutil.MustString(flags, "format") 84 | leaf := flagutil.MustBool(flags, "leaf") 85 | in := flagutil.MustString(flags, "in") 86 | newKey := flagutil.MustBool(flags, "new") 87 | kty := flagutil.MustString(flags, "kty") 88 | crv := flagutil.MustString(flags, "crv") 89 | size := flagutil.MustInt(flags, "size") 90 | alg := flagutil.MustString(flags, "alg") 91 | kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms")) 92 | if kuri == "" { 93 | kuri = name 94 | } 95 | 96 | km, err := openKMS(cmd.Context(), kuri) 97 | if err != nil { 98 | return fmt.Errorf("failed to load key manager: %w", err) 99 | } 100 | defer km.Close() 101 | 102 | if format == "tpm" && newKey { 103 | if kty != "RSA" { 104 | size = 0 105 | } 106 | // Do not set crv unless the flag is explicitly set by the user 107 | if kty != "EC" && !flags.Changed("crv") { 108 | crv = "" 109 | } 110 | signatureAlgorithm := getSignatureAlgorithm(kty, crv, alg, false) 111 | if signatureAlgorithm == apiv1.UnspecifiedSignAlgorithm { 112 | return fmt.Errorf("failed to get a signature algorithm with kty: %q, crv: %q, hash: %q", kty, crv, alg) 113 | } 114 | 115 | // TODO(hs): support reading the attesting data (key authorization / qualifying data) 116 | // from stdin. Currently it needs to be provided as part of the key URI (e.g. qualifying-data=), 117 | // for TPMs, but for the other formats, it is read from stdout. This would require 118 | // a new property in the CreateKeyRequest, or changing the value of `name`. 119 | resp, err := km.CreateKey(&apiv1.CreateKeyRequest{ 120 | Name: name, 121 | SignatureAlgorithm: signatureAlgorithm, 122 | Bits: size, 123 | }) 124 | if err != nil { 125 | return err 126 | } 127 | name = resp.Name // continue with updated name 128 | } 129 | 130 | attester, ok := km.(apiv1.Attester) 131 | if !ok { 132 | return fmt.Errorf("%s does not implement Attester", kuri) 133 | } 134 | 135 | resp, err := attester.CreateAttestation(&apiv1.CreateAttestationRequest{ 136 | Name: name, 137 | }) 138 | if err != nil { 139 | return fmt.Errorf("failed to attest: %w", err) 140 | } 141 | 142 | switch { 143 | case format != "": 144 | var data []byte 145 | var signer crypto.Signer 146 | if format != "tpm" { // the tpm format doesn't require data to be signed 147 | data, err = getAttestationData(in) 148 | if err != nil { 149 | return err 150 | } 151 | } 152 | if signer, err = km.CreateSigner(&apiv1.CreateSignerRequest{ 153 | SigningKey: name, 154 | }); err != nil { 155 | return fmt.Errorf("failed to get a signer: %w", err) 156 | } 157 | var certs []*x509.Certificate 158 | switch { 159 | case len(resp.CertificateChain) > 0: 160 | certs = resp.CertificateChain 161 | case resp.Certificate != nil: 162 | certs = []*x509.Certificate{resp.Certificate} 163 | } 164 | return printAttestationObject(format, certs, signer, data, resp.CertificationParameters) 165 | case len(resp.CertificateChain) > 0: 166 | switch { 167 | case leaf: 168 | return outputCert(resp.CertificateChain[0]) 169 | default: 170 | for _, c := range resp.CertificateChain { 171 | if err := outputCert(c); err != nil { 172 | return err 173 | } 174 | } 175 | } 176 | return nil 177 | case resp.Certificate != nil: 178 | return outputCert(resp.Certificate) 179 | case resp.PublicKey != nil: 180 | block, err := pemutil.Serialize(resp.PublicKey) 181 | if err != nil { 182 | return err 183 | } 184 | return pem.Encode(os.Stdout, block) 185 | default: 186 | return errors.New("failed to create attestation: unsupported response") 187 | } 188 | }, 189 | } 190 | 191 | type attestationObject struct { 192 | Format string `json:"fmt"` 193 | AttStatement map[string]interface{} `json:"attStmt,omitempty"` 194 | } 195 | 196 | func getAttestationData(in string) ([]byte, error) { 197 | if in != "" { 198 | return os.ReadFile(in) 199 | } 200 | fi, err := os.Stdin.Stat() 201 | if err != nil { 202 | return nil, err 203 | } 204 | if (fi.Mode() & os.ModeCharDevice) == 0 { 205 | return io.ReadAll(os.Stdin) 206 | } 207 | fmt.Println("Type data to sign and press Ctrl+D to finish:") 208 | return io.ReadAll(os.Stdin) 209 | } 210 | 211 | func printAttestationObject(format string, certs []*x509.Certificate, signer crypto.Signer, data []byte, params *apiv1.CertificationParameters) error { 212 | var alg int64 213 | var digest []byte 214 | var opts crypto.SignerOpts 215 | switch k := signer.Public().(type) { 216 | case *ecdsa.PublicKey: 217 | if k.Curve != elliptic.P256() { 218 | return fmt.Errorf("unsupported elliptic curve %s", k.Curve) 219 | } 220 | alg = -7 // ES256 221 | opts = crypto.SHA256 222 | sum := sha256.Sum256(data) 223 | digest = sum[:] 224 | case *rsa.PublicKey: 225 | // TODO(mariano): support for PS256 (-37) 226 | alg = -257 // RS256 227 | opts = crypto.SHA256 228 | sum := sha256.Sum256(data) 229 | digest = sum[:] 230 | case ed25519.PublicKey: 231 | alg = -8 // EdDSA 232 | opts = crypto.Hash(0) 233 | digest = data 234 | default: 235 | return fmt.Errorf("unsupported public key type %T", k) 236 | } 237 | 238 | stmt := map[string]interface{}{ 239 | "alg": alg, 240 | } 241 | 242 | switch format { 243 | case "tpm": 244 | // TPM key attestation is performed at key creation time. The key is attested by 245 | // an Attestation Key (AK). The result of attesting a key can be recorded, so that 246 | // the certification facts can be used at a later time to verify the key was created 247 | // by a specific TPM. 248 | if params == nil { 249 | return errors.New("TPM key attestation requires CertificationParameters to be set") 250 | } 251 | stmt["ver"] = "2.0" 252 | stmt["sig"] = params.CreateSignature // signature over the (empty) data is ignored for the tpm format 253 | stmt["certInfo"] = params.CreateAttestation 254 | stmt["pubArea"] = params.Public 255 | default: 256 | // Sign proves possession of private key. Per recommendation at 257 | // https://w3c.github.io/webauthn/#sctn-signature-attestation-types, we use 258 | // CBOR to encode the signature. 259 | sig, err := signer.Sign(rand.Reader, digest, opts) 260 | if err != nil { 261 | return fmt.Errorf("failed to sign key authorization: %w", err) 262 | } 263 | sig, err = cbor.Marshal(sig) 264 | if err != nil { 265 | return fmt.Errorf("failed marshaling signature: %w", err) 266 | } 267 | stmt["sig"] = sig 268 | } 269 | 270 | if len(certs) > 0 { 271 | x5c := make([][]byte, len(certs)) 272 | for i, c := range certs { 273 | x5c[i] = c.Raw 274 | } 275 | stmt["x5c"] = x5c 276 | } 277 | 278 | obj := attestationObject{ 279 | Format: format, 280 | AttStatement: stmt, 281 | } 282 | 283 | b, err := cbor.Marshal(obj) 284 | if err != nil { 285 | return fmt.Errorf("failed marshaling attestation object: %w", err) 286 | } 287 | 288 | fmt.Println(base64.RawURLEncoding.EncodeToString(b)) 289 | return nil 290 | } 291 | 292 | func init() { 293 | rootCmd.AddCommand(attestCmd) 294 | attestCmd.SilenceUsage = true 295 | 296 | flags := attestCmd.Flags() 297 | flags.SortFlags = false 298 | 299 | // TODO(hs): fix/validate valid values for TPM 300 | kty := flagutil.UpperValue("kty", []string{"EC", "RSA"}, "RSA") 301 | crv := flagutil.NormalizedValue("crv", []string{"P256", "P384", "P521"}, "P256") 302 | alg := flagutil.NormalizedValue("alg", []string{"SHA256", "SHA384", "SHA512"}, "SHA256") 303 | 304 | format := flagutil.LowerValue("format", []string{"", "step", "packed", "tpm"}, "") 305 | flags.Var(format, "format", "The `format` to print the attestation.\nOptions are step, packed or tpm") 306 | flags.Bool("leaf", false, "Print only the leaf certificate in a chain") 307 | flags.Bool("new", false, "(EXPERIMENTAL) Creates and attests a new key instead of attesting an existing one") 308 | flags.Var(kty, "kty", "The key `type` to build the certificate upon.\nOptions are EC and RSA. Only used with TPMKMS.") 309 | flags.Var(crv, "crv", "The elliptic `curve` to use for EC and OKP key types.\nOptions are P256, P384 and P521. Only used with TPMKMS.") 310 | flags.Int("size", 2048, "The key size for an RSA key") // TODO(hs): attesting 3072 bit RSA keys on TPM that doesn't support it returns an ugly error; we want to catch that earlier. 311 | flags.Var(alg, "alg", "The hashing `algorithm` to use with RSA PKCS #1 signatures.\nOptions are SHA256, SHA384 or SHA512. Only used with TPMKMS.") 312 | flags.String("in", "", "The `file` to sign with an attestation format.") 313 | } 314 | -------------------------------------------------------------------------------- /cmd/certificate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "fmt" 18 | "io/fs" 19 | "net/url" 20 | "strings" 21 | 22 | "github.com/spf13/cobra" 23 | "go.step.sm/crypto/kms" 24 | "go.step.sm/crypto/kms/apiv1" 25 | "go.step.sm/crypto/kms/uri" 26 | "go.step.sm/crypto/pemutil" 27 | 28 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 29 | ) 30 | 31 | // certificateCmd represents the certificate command 32 | var certificateCmd = &cobra.Command{ 33 | Use: "certificate ", 34 | Short: "print or import a certificate in a KMS", 35 | Long: `This command, if the KMS supports it, prints or imports a certificate in a KMS.`, 36 | Example: ` # Import a certificate to a PKCS #11 module: 37 | step-kms-plugin certificate --import cert.pem \ 38 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 39 | 'pkcs11:id=2000;object=my-cert' 40 | 41 | # Print a previously stored certificate: 42 | step-kms-plugin certificate \ 43 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 44 | 'pkcs11:id=2000;object=my-cert' 45 | 46 | # Import a certificate for an Attestation Key (AK), using the default TPM KMS: 47 | step-kms-plugin certificate --import cert.pem 'tpmkms:name=my-ak;ak=true' 48 | 49 | # Import a certificate, using the default TPM KMS: 50 | step-kms-plugin certificate --import cert.pem tpmkms:name=my-key 51 | 52 | # Print a previously stored certificate for an Attestation Key (AK), using the default TPM KMS: 53 | step-kms-plugin certificate 'tpmkms:name=my-ak;ak=true' 54 | 55 | # Print a previously stored certificate, using the default TPM KMS: 56 | step-kms-plugin certificate tpmkms:name=my-key 57 | 58 | # Print a previously stored certificate chain, using the default TPM KMS: 59 | step-kms-plugin certificate --bundle tpmkms:name=my-key`, 60 | RunE: func(cmd *cobra.Command, args []string) error { 61 | if len(args) != 1 { 62 | return showErrUsage(cmd) 63 | } 64 | 65 | flags := cmd.Flags() 66 | certFile := flagutil.MustString(flags, "import") 67 | bundle := flagutil.MustBool(flags, "bundle") 68 | 69 | kuri, name, err := getURIAndNameForFS(flagutil.MustString(flags, "kms"), args[0]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Read a certificate using the CertFS. 75 | if certFile == "" { 76 | if bundle { 77 | km, err := openKMS(cmd.Context(), kuri) 78 | if err != nil { 79 | return fmt.Errorf("failed to load key manager: %w", err) 80 | } 81 | defer km.Close() 82 | if cm, ok := km.(apiv1.CertificateChainManager); ok { 83 | certs, err := cm.LoadCertificateChain(&apiv1.LoadCertificateChainRequest{ 84 | Name: name, 85 | }) 86 | if err != nil { 87 | return err 88 | } 89 | for _, c := range certs { 90 | outputCert(c) 91 | } 92 | return nil 93 | } 94 | return fmt.Errorf("--bundle is not compatible with %q", kuri) 95 | } 96 | 97 | // TODO(hs): support reading a certificate chain / bundle instead of 98 | // just single certificate in the CertFS instead? Would require supporting 99 | // serializing multiple things to PEM, e.g. a certificate chain. 100 | fsys, err := kms.CertFS(cmd.Context(), kuri) 101 | if err != nil { 102 | return err 103 | } 104 | defer fsys.Close() 105 | 106 | b, err := fs.ReadFile(fsys, name) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | fmt.Print(string(b)) 112 | return nil 113 | } 114 | 115 | // Import and read certificate using the key manager to avoid opening the kms twice. 116 | certs, err := pemutil.ReadCertificateBundle(certFile) 117 | if err != nil { 118 | return err 119 | } 120 | if len(certs) == 0 { 121 | return fmt.Errorf("no certificates found in %q", certFile) 122 | } 123 | cert := certs[0] 124 | 125 | km, err := openKMS(cmd.Context(), kuri) 126 | if err != nil { 127 | return fmt.Errorf("failed to load key manager: %w", err) 128 | } 129 | defer km.Close() 130 | 131 | // On mackms there's no need to specify a label (name), the keychain 132 | // will automatically use the common name by default. But we always need 133 | // a label to load the certificate. 134 | loadCertificateName := name 135 | if strings.EqualFold(loadCertificateName, "mackms:") { 136 | loadCertificateName = uri.New("mackms", url.Values{ 137 | "label": []string{cert.Subject.CommonName}, 138 | }).String() 139 | } 140 | 141 | switch cm := km.(type) { 142 | case apiv1.CertificateChainManager: 143 | if err := cm.StoreCertificateChain(&apiv1.StoreCertificateChainRequest{ 144 | Name: name, 145 | CertificateChain: certs, 146 | }); err != nil { 147 | return err 148 | } 149 | certs, err = cm.LoadCertificateChain(&apiv1.LoadCertificateChainRequest{ 150 | Name: loadCertificateName, 151 | }) 152 | if err != nil { 153 | return err 154 | } 155 | cert = certs[0] 156 | case apiv1.CertificateManager: 157 | if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{ 158 | Name: name, 159 | Certificate: cert, 160 | }); err != nil { 161 | return err 162 | } 163 | cert, err = cm.LoadCertificate(&apiv1.LoadCertificateRequest{ 164 | Name: loadCertificateName, 165 | }) 166 | if err != nil { 167 | return err 168 | } 169 | default: 170 | return fmt.Errorf("%q does not implement a CertificateManager or CertificateChainManager", kuri) 171 | } 172 | 173 | switch { 174 | case bundle: 175 | for _, c := range certs { 176 | if err := outputCert(c); err != nil { 177 | return err 178 | } 179 | } 180 | return nil 181 | default: 182 | return outputCert(cert) 183 | } 184 | }, 185 | } 186 | 187 | func init() { 188 | rootCmd.AddCommand(certificateCmd) 189 | certificateCmd.SilenceUsage = true 190 | 191 | flags := certificateCmd.Flags() 192 | flags.SortFlags = false 193 | 194 | flags.String("import", "", "The certificate `file` to import") 195 | flags.Bool("bundle", false, "Print all certificates in the chain/bundle") 196 | } 197 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "crypto/rsa" 18 | "crypto/x509" 19 | "encoding/json" 20 | "encoding/pem" 21 | "fmt" 22 | "net/url" 23 | "strings" 24 | 25 | "github.com/spf13/cobra" 26 | "go.step.sm/crypto/kms/apiv1" 27 | "go.step.sm/crypto/kms/softkms" 28 | "go.step.sm/crypto/kms/tpmkms" 29 | "go.step.sm/crypto/pemutil" 30 | "go.step.sm/crypto/tpm/tss2" 31 | 32 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 33 | "github.com/smallstep/step-kms-plugin/internal/termutil" 34 | ) 35 | 36 | // createCmd represents the create command 37 | var createCmd = &cobra.Command{ 38 | Use: "create ", 39 | Short: "generates a key pair in the KMS", 40 | Long: `Creates a private key in the KMS and prints its public key. 41 | 42 | This command creates a new asymmetric key pair on the KMS. By default, 43 | it creates an EC P-256 key, but the --kty, --crv and --size flags can be 44 | combined to adjust the key properties. RSA and EC keys are broadly 45 | supported, but as of 2023 Ed25519 (OKP) support is very limited. 46 | 47 | For keys in AWS KMS, we recommend using --json for output, as you will need the 48 | generated key-id. 49 | 50 | Keys in a PKCS #11 module requires an id in hexadecimal as well as a label 51 | (e.g. pkcs11:id=10ab;object=my-label).`, 52 | Example: ` # Create an EC P-256 private key in a PKCS #11 module: 53 | step-kms-plugin create \ 54 | 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm;id=1000;object=my-key?pin-value=pass' 55 | 56 | # Create an EC P-384 private key in a PKCS #11 module: 57 | step-kms-plugin create --kty EC --crv P-384 \ 58 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-source=/var/run/pass.txt' \ 59 | 'pkcs11:id=1000;object=my-key' 60 | 61 | # Create an 3072-bit RSA key in a PKCS#11 module: 62 | step-kms-plugin create --kty RSA \ 63 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 64 | 'pkcs11:id=1000;object=my-rsa-key' 65 | 66 | # Create a key on Google's Cloud KMS using gcloud credentials: 67 | step-kms-plugin create cloudkms:projects/my-project/locations/us-west1/keyRings/my-keyring/cryptoKeys/my-ec-key 68 | 69 | # Create a 4096-bit RSA-PSS key on Google's Cloud KMS with a credentials file: 70 | step-kms-plugin create --kty RSA --size 4096 --pss \ 71 | --kms cloudkms:credentials-file=kms-credentials.json \ 72 | projects/my-project/locations/us-west1/keyRings/my-keyring/cryptoKeys/my-rsa-key 73 | 74 | # Create a key on Azure's Key Vault using az credentials: 75 | step-kms-plugin create 'azurekms:vault=my-key-vault;name=my-key' 76 | 77 | # Create a key on AWS KMS with the name tag my-key. Return the value in JSON to get the uri used to access the key: 78 | step-kms-plugin create --json awskms:name=my-key 79 | 80 | # Create a 2048-bit RSA key on a YubiKey: 81 | step-kms-plugin create --kty RSA --size 2048 yubikey:slot-id=82 82 | 83 | # Create an EC P-256 private key on a YubiKey with the touch policy "always" and pin policy "once": 84 | step-kms-plugin create --touch-policy always --pin-policy once yubikey:slot-id=82 85 | 86 | # Create an Attestation Key (AK) in the default TPM KMS: 87 | step-kms-plugin create --kty RSA --size 2048 'tpmkms:name=my-ak;ak=true' 88 | 89 | # Create an EC P-256 private key in the default TPM KMS and print it using the TSS2 PEM format: 90 | step-kms-plugin create --format TSS2 tpmkms:name=my-ec-key 91 | 92 | # Create an EC P-256 private key in the TPM KMS, backed by /tmp/tpmobjects: 93 | step-kms-plugin create my-tmp-ec-key --kms tpmkms:storage-directory=/tmp/tpmobjects 94 | 95 | # Create an RSA 4096 bits private key in the default TPM KMS: 96 | step-kms-plugin create --kty RSA --size 4096 tpmkms:name=my-rsa-key 97 | 98 | # Create an EC P-256 private key, attested by an AK, in the default TPM KMS: 99 | step-kms-plugin create 'tpmkms:name=my-ec-key;attest-by=my-ak'`, 100 | 101 | RunE: func(cmd *cobra.Command, args []string) error { 102 | if len(args) != 1 { 103 | return showErrUsage(cmd) 104 | } 105 | 106 | flags := cmd.Flags() 107 | name := args[0] 108 | kty := flagutil.MustString(flags, "kty") 109 | crv := flagutil.MustString(flags, "crv") 110 | size := flagutil.MustInt(flags, "size") 111 | alg := flagutil.MustString(flags, "alg") 112 | pss := flagutil.MustBool(flags, "pss") 113 | extractable := flagutil.MustBool(flags, "extractable") 114 | pl := flagutil.MustString(flags, "protection-level") 115 | pinPolicy := pinPolicyMapping[flagutil.MustString(flags, "pin-policy")] 116 | touchPolicy := touchPolicyMapping[flagutil.MustString(flags, "touch-policy")] 117 | 118 | // Do not set crv unless the flag is explicitly set by the user 119 | if kty != "EC" && !flags.Changed("crv") { 120 | crv = "" 121 | } 122 | // Set kty RSA if the pss flag is passed 123 | if pss { 124 | if !flags.Changed("kty") { 125 | kty = "RSA" 126 | } else if kty != "RSA" { 127 | return fmt.Errorf("flag --pss is incompatible with --kty %s", kty) 128 | } 129 | } 130 | // Set the size to 0 for non-RSA keys 131 | if kty != "RSA" { 132 | size = 0 133 | } 134 | 135 | signatureAlgorithm := getSignatureAlgorithm(kty, crv, alg, pss) 136 | if signatureAlgorithm == apiv1.UnspecifiedSignAlgorithm { 137 | return fmt.Errorf("failed to get a signature algorithm with kty: %q, crv: %q, hash: %q", kty, crv, alg) 138 | } 139 | 140 | protectionLevel := getProtectionLevel(pl) 141 | if protectionLevel == apiv1.UnspecifiedProtectionLevel { 142 | return fmt.Errorf("unsupported protection level %q", pl) 143 | } 144 | 145 | kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms")) 146 | if kuri == "" { 147 | kuri = name 148 | } 149 | 150 | cmd.SilenceUsage = true 151 | km, err := openKMS(cmd.Context(), kuri) 152 | if err != nil { 153 | return fmt.Errorf("failed to load key manager: %w", err) 154 | } 155 | defer km.Close() 156 | 157 | if _, ok := km.(*tpmkms.TPMKMS); ok { 158 | if flagutil.MustString(flags, "format") == "TSS2" { 159 | name, err = changeURI(name, url.Values{"tss2": []string{"true"}}) 160 | if err != nil { 161 | return fmt.Errorf("failed to parse %q: %w", name, err) 162 | } 163 | } 164 | } 165 | 166 | resp, err := km.CreateKey(&apiv1.CreateKeyRequest{ 167 | Name: name, 168 | SignatureAlgorithm: signatureAlgorithm, 169 | Bits: size, 170 | ProtectionLevel: protectionLevel, 171 | Extractable: extractable, 172 | PINPolicy: pinPolicy, 173 | TouchPolicy: touchPolicy, 174 | }) 175 | if err != nil { 176 | return fmt.Errorf("failed to create key: %w", err) 177 | } 178 | 179 | // Store the private key on disk if softkms is used 180 | _, isSoftKMS := km.(*softkms.SoftKMS) 181 | if isSoftKMS && resp.PrivateKey != nil { 182 | block, err := pemutil.Serialize(resp.PrivateKey) 183 | if err != nil { 184 | return fmt.Errorf("failed to serialize the private key: %w", err) 185 | } 186 | if err := termutil.WriteFile(resp.Name, pem.EncodeToMemory(block), 0600); err != nil { 187 | return fmt.Errorf("failed to write the private key: %w", err) 188 | } 189 | } 190 | 191 | return printCreateKeyResponse(cmd, resp) 192 | }, 193 | } 194 | 195 | func printCreateKeyResponse(cmd *cobra.Command, resp *apiv1.CreateKeyResponse) error { 196 | var ( 197 | s string 198 | isPrivateKey bool 199 | flags = cmd.Flags() 200 | ) 201 | 202 | switch flagutil.MustString(flags, "format") { 203 | case "PKCS1": 204 | if key, ok := resp.PublicKey.(*rsa.PublicKey); ok { 205 | s = string(pem.EncodeToMemory(&pem.Block{ 206 | Type: "RSA PUBLIC KEY", Bytes: x509.MarshalPKCS1PublicKey(key), 207 | })) 208 | } 209 | case "TSS2": 210 | if key, ok := resp.PrivateKey.(*tss2.TPMKey); ok { 211 | b, err := key.EncodeToMemory() 212 | if err != nil { 213 | return fmt.Errorf("failed to serialize the private key: %w", err) 214 | } 215 | s = string(b) 216 | isPrivateKey = true 217 | } 218 | } 219 | 220 | // Encode public key using PKIX format 221 | if s == "" { 222 | block, err := pemutil.Serialize(resp.PublicKey) 223 | if err != nil { 224 | return fmt.Errorf("failed to serialize the public key: %w", err) 225 | } 226 | s = string(pem.EncodeToMemory(block)) 227 | } 228 | 229 | if flagutil.MustBool(flags, "json") { 230 | m := map[string]string{ 231 | "name": resp.Name, 232 | } 233 | if isPrivateKey { 234 | m["privateKey"] = s 235 | } else { 236 | m["publicKey"] = s 237 | } 238 | 239 | b, err := json.MarshalIndent(m, "", " ") 240 | if err != nil { 241 | return fmt.Errorf("failed to marshal: %w", err) 242 | } 243 | fmt.Println(string(b)) 244 | } else { 245 | fmt.Print(s) 246 | } 247 | 248 | return nil 249 | } 250 | 251 | type rsaParams struct { 252 | alg string 253 | pss bool 254 | } 255 | 256 | var rsaSignatureAlgorithmMapping = map[rsaParams]apiv1.SignatureAlgorithm{ 257 | {"", false}: apiv1.SHA256WithRSA, 258 | {"SHA256", false}: apiv1.SHA256WithRSA, 259 | {"SHA384", false}: apiv1.SHA384WithRSA, 260 | {"SHA512", false}: apiv1.SHA512WithRSA, 261 | {"SHA256", true}: apiv1.SHA256WithRSAPSS, 262 | {"SHA384", true}: apiv1.SHA384WithRSAPSS, 263 | {"SHA512", true}: apiv1.SHA512WithRSAPSS, 264 | } 265 | 266 | type ecParams struct { 267 | crv string 268 | } 269 | 270 | var ecSignatureAlgorithmMapping = map[ecParams]apiv1.SignatureAlgorithm{ 271 | {""}: apiv1.ECDSAWithSHA256, 272 | {"P256"}: apiv1.ECDSAWithSHA256, 273 | {"P384"}: apiv1.ECDSAWithSHA384, 274 | {"P521"}: apiv1.ECDSAWithSHA512, 275 | } 276 | 277 | type okpParams struct { 278 | crv string 279 | } 280 | 281 | var okpSignatureAlgorithmMapping = map[okpParams]apiv1.SignatureAlgorithm{ 282 | {""}: apiv1.PureEd25519, 283 | {"ED25519"}: apiv1.PureEd25519, 284 | } 285 | 286 | var pinPolicyMapping = map[string]apiv1.PINPolicy{ 287 | "": 0, // Use default on YubiKey kms (always) 288 | "NEVER": apiv1.PINPolicyNever, 289 | "ALWAYS": apiv1.PINPolicyAlways, 290 | "ONCE": apiv1.PINPolicyOnce, 291 | } 292 | 293 | var touchPolicyMapping = map[string]apiv1.TouchPolicy{ 294 | "": 0, // Use default on YubiKey kms (never) 295 | "NEVER": apiv1.TouchPolicyNever, 296 | "ALWAYS": apiv1.TouchPolicyAlways, 297 | "CACHED": apiv1.TouchPolicyCached, 298 | } 299 | 300 | func getSignatureAlgorithm(kty, crv, alg string, pss bool) apiv1.SignatureAlgorithm { 301 | switch strings.ToUpper(kty) { 302 | case "EC": 303 | return ecSignatureAlgorithmMapping[ecParams{crv}] 304 | case "RSA": 305 | return rsaSignatureAlgorithmMapping[rsaParams{alg, pss}] 306 | case "OKP": 307 | return okpSignatureAlgorithmMapping[okpParams{crv}] 308 | default: 309 | return ecSignatureAlgorithmMapping[ecParams{crv}] 310 | } 311 | } 312 | 313 | func getProtectionLevel(pl string) apiv1.ProtectionLevel { 314 | switch strings.ToUpper(pl) { 315 | case "", "SOFTWARE": 316 | return apiv1.Software 317 | case "HSM", "HARDWARE": 318 | return apiv1.HSM 319 | default: 320 | return apiv1.UnspecifiedProtectionLevel 321 | } 322 | } 323 | 324 | func init() { 325 | rootCmd.AddCommand(createCmd) 326 | createCmd.SilenceUsage = true 327 | 328 | flags := createCmd.Flags() 329 | flags.SortFlags = false 330 | 331 | kty := flagutil.UpperValue("kty", []string{"EC", "RSA", "OKP"}, "EC") 332 | crv := flagutil.NormalizedValue("crv", []string{"P256", "P384", "P521", "Ed25519"}, "P256") 333 | alg := flagutil.NormalizedValue("alg", []string{"SHA256", "SHA384", "SHA512"}, "SHA256") 334 | format := flagutil.NormalizedValue("format", []string{"PKIX", "PKCS1", "TSS2"}, "PKIX") 335 | protectionLevel := flagutil.UpperValue("protection-level", []string{"SOFTWARE", "HSM"}, "SOFTWARE") 336 | pinPolicy := flagutil.UpperValue("pin-policy", []string{"NEVER", "ALWAYS", "ONCE"}, "") 337 | touchPolicy := flagutil.UpperValue("touch-policy", []string{"NEVER", "ALWAYS", "CACHED"}, "") 338 | 339 | flags.Var(kty, "kty", "The key `type` to build the certificate upon.\nOptions are EC, RSA or OKP") 340 | flags.Var(crv, "crv", "The elliptic `curve` to use for EC and OKP key types.\nOptions are P256, P384, P521 or Ed25519 on OKP") 341 | flags.Int("size", 3072, "The key size for an RSA key") 342 | flags.Var(alg, "alg", "The hashing `algorithm` to use on RSA PKCS #1 and RSA-PSS signatures.\nOptions are SHA256, SHA384 or SHA512") 343 | flags.Var(protectionLevel, "protection-level", "The protection `level` used on some Cloud KMSs.\nOptions are SOFTWARE or HSM") 344 | flags.Var(pinPolicy, "pin-policy", "The pin `policy` used on YubiKey KMS.\nOptions are NEVER, ALWAYS or ONCE") 345 | flags.Var(touchPolicy, "touch-policy", "The touch `policy` used on YubiKey KMS.\nOptions are NEVER, ALWAYS or CACHED") 346 | flags.Bool("pss", false, "Use RSA-PSS signature scheme instead of PKCS #1") 347 | flags.Bool("extractable", false, "Mark the new key as extractable") 348 | flags.Var(format, "format", "The `format` to use in the output.\nOptions are PKIX, PKCS1 or TSS2") 349 | flags.Bool("json", false, "Show the output using JSON") 350 | } 351 | -------------------------------------------------------------------------------- /cmd/decrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "bytes" 18 | "crypto" 19 | "crypto/rand" 20 | "crypto/rsa" 21 | "encoding/base64" 22 | "encoding/hex" 23 | "fmt" 24 | "io" 25 | "os" 26 | 27 | "github.com/spf13/cobra" 28 | "go.step.sm/crypto/kms/apiv1" 29 | 30 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 31 | ) 32 | 33 | // decryptCmd represents the decrypt command 34 | var decryptCmd = &cobra.Command{ 35 | Use: "decrypt ", 36 | Short: "decrypt the given input with an RSA key", 37 | Long: `Decrypts the given input with an RSA private key present in a KMS. 38 | 39 | This command supports decrypting a short encrypted message (e.g. a password) with 40 | RSA and the padding scheme from PKCS #1 v1.5 or using RSA-OAEP. 41 | 42 | Not all devices support both schemes. YubiKeys do: they support PKCS #1 v1.5 via 43 | the PIV application, and they support RSA-OAEP via the YubiKey PKCS #11 library, 44 | YKCS11. Other PKCS #11 devices (including YubiHSM2) will generally support both 45 | PKCS #1 v.1.5 and RSA-OAEP. Google Cloud KMS only supports RSA-OAEP and doesn't 46 | support labels, so you should use "--no-label" when encrypting to a key in Google 47 | Cloud KMS.`, 48 | Example: ` # Decrypts a input given by stdin using RSA PKCS#1 v1.5: 49 | cat message.b64 | step-kms-plugin decrypt yubikey:slot-id=82 50 | 51 | # Decrypts a given file using RSA-OAEP: 52 | step-kms-plugin decrypt --oaep --in message.b64 \ 53 | --kms 'pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM?pin-value=0001password' \ 54 | 'pkcs11:object=my-rsa-key' 55 | 56 | # Decrypts a given file using RSA-OAEP and no label: 57 | step-kms-plugin decrypt --oaep --in message.b64 --no-label \ 58 | --kms 'cloudkms:' \ 59 | 'projects/my-project-id/locations/global/keyRings/my-decrypter-ring/cryptoKeys/my-decrypter/cryptoKeyVersions/1' 60 | 61 | 62 | # Decrypts a given file using RSA-OAEP and a custom label: 63 | step-kms-plugin decrypt --oaep --in message.b64 label my-custom-label \ 64 | --kms 'pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM?pin-value=0001password' \ 65 | 'pkcs11:object=my-rsa-key' 66 | 67 | 68 | # Decrypts a given file encoded in hexadecimal format from a file in disk: 69 | step-kms-plugin decrypt --format hex --in message.hex --kms softkms: rsa.priv`, 70 | RunE: func(cmd *cobra.Command, args []string) error { 71 | if l := len(args); l != 1 { 72 | return showErrUsage(cmd) 73 | } 74 | 75 | name := args[0] 76 | flags := cmd.Flags() 77 | kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms")) 78 | if kuri == "" { 79 | kuri = name 80 | } 81 | 82 | oaep := flagutil.MustBool(flags, "oaep") 83 | label := flagutil.MustString(flags, "label") 84 | noLabel := flagutil.MustBool(flags, "no-label") 85 | format := flagutil.MustString(flags, "format") 86 | in := flagutil.MustString(flags, "in") 87 | 88 | // OAEP requires a hash algorithm. 89 | // It uses SHA256 by default. 90 | var hash crypto.Hash 91 | var err error 92 | if oaep { 93 | alg := flagutil.MustString(flags, "alg") 94 | if hash, err = getHashAlgorithm(alg); err != nil { 95 | return err 96 | } 97 | } 98 | 99 | var src, data []byte 100 | if in != "" { 101 | if src, err = os.ReadFile(in); err != nil { 102 | return fmt.Errorf("failed to read file %q: %w", in, err) 103 | } 104 | } else { 105 | if src, err = io.ReadAll(os.Stdin); err != nil { 106 | return fmt.Errorf("failed to read from STDIN: %w", err) 107 | } 108 | } 109 | 110 | switch format { 111 | case "hex": 112 | src = bytes.TrimSpace(src) 113 | size := hex.DecodedLen(len(src)) 114 | data = make([]byte, size) 115 | n, err := hex.Decode(data, src) 116 | if err != nil { 117 | return fmt.Errorf("failed to decode input: %w", err) 118 | } 119 | data = data[:n] 120 | case "raw": 121 | data = src 122 | default: 123 | size := base64.StdEncoding.DecodedLen(len(src)) 124 | data = make([]byte, size) 125 | n, err := base64.StdEncoding.Decode(data, src) 126 | if err != nil { 127 | return fmt.Errorf("failed to decode input: %w", err) 128 | } 129 | data = data[:n] 130 | } 131 | 132 | km, err := openKMS(cmd.Context(), kuri) 133 | if err != nil { 134 | return fmt.Errorf("failed to load key manager: %w", err) 135 | } 136 | defer km.Close() 137 | 138 | dec, ok := km.(apiv1.Decrypter) 139 | if !ok { 140 | return fmt.Errorf("%s does not implement Decrypter", kuri) 141 | } 142 | 143 | d, err := dec.CreateDecrypter(&apiv1.CreateDecrypterRequest{ 144 | DecryptionKey: name, 145 | }) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | var opts crypto.DecrypterOpts 151 | if oaep { 152 | var oaepLabel []byte 153 | switch { 154 | case noLabel: 155 | break // nothing to do 156 | default: 157 | oaepLabel = []byte(label) 158 | } 159 | opts = &rsa.OAEPOptions{ 160 | Hash: hash, 161 | Label: oaepLabel, 162 | } 163 | } 164 | 165 | b, err := d.Decrypt(rand.Reader, data, opts) 166 | if err != nil { 167 | return fmt.Errorf("error decrypting input: %w", err) 168 | } 169 | os.Stdout.Write(b) 170 | return nil 171 | }, 172 | } 173 | 174 | func init() { 175 | rootCmd.AddCommand(decryptCmd) 176 | decryptCmd.SilenceUsage = true 177 | 178 | flags := decryptCmd.Flags() 179 | flags.SortFlags = false 180 | 181 | alg := flagutil.NormalizedValue("alg", []string{"SHA256", "SHA384", "SHA512"}, "SHA256") 182 | format := flagutil.LowerValue("format", []string{"base64", "hex", "raw"}, "base64") 183 | 184 | flags.Bool("oaep", false, "Use RSA-OAEP instead of RSA PKCS #1 v1.5") 185 | flags.Bool("no-label", false, "Omit the label when RSA-OAEP is used") 186 | flags.String("label", DefaultOEAPLabel, "Set a label when using RSA-OAEP") 187 | flags.Var(alg, "alg", "The hashing `algorithm` to use on RSA-OAEP.\nOptions are SHA256, SHA384 or SHA512") 188 | flags.Var(format, "format", "The `format` to print the signature.\nOptions are base64, hex, or raw") 189 | flags.String("in", "", "The `file` to decrypt instead of using STDIN.") 190 | } 191 | -------------------------------------------------------------------------------- /cmd/encrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "crypto" 18 | "crypto/rand" 19 | "crypto/rsa" 20 | "encoding/base64" 21 | "encoding/hex" 22 | "fmt" 23 | "io" 24 | "os" 25 | 26 | "github.com/spf13/cobra" 27 | "go.step.sm/crypto/kms/apiv1" 28 | 29 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 30 | ) 31 | 32 | // DefaultOEAPLabel is the label used when OAEP is used. 33 | const DefaultOEAPLabel = "step-kms-plugin/v1/oaep" 34 | 35 | // encryptCmd represents the encrypt command 36 | var encryptCmd = &cobra.Command{ 37 | Use: "encrypt ", 38 | Short: "encrypt a given input with an RSA public key", 39 | Long: `Encrypts a given input with an RSA public key. 40 | 41 | This command supports encrypting a short message (eg. a password) with RSA and 42 | the padding scheme from PKCS #1 v1.5 or using RSA-OAEP. The messages must not be 43 | longer than the size of the key minus a number of bytes that depend on the 44 | scheme used. 45 | 46 | All KMSs support encryption, because only the public key is used. Support for 47 | decryption is currently limited to YubiKey, Google Cloud KMS and some PKCS #11 KMSs. 48 | Not all devices support both schemes. YubiKeys do: they support PKCS #1 v1.5 via the PIV 49 | application, and they support RSA-OAEP via the YubiKey PKCS #11 library, YKCS11. 50 | Other PKCS #11 devices (including YubiHSM2) will generally support both 51 | PKCS #1 v.1.5 and RSA-OAEP. Google Cloud KMS only supports RSA-OAEP and doesn't 52 | support labels, so you should use "--no-label" when encrypting to a key in Google 53 | Cloud KMS.`, 54 | Example: ` # Encrypt a password given by stdin using RSA PKCS#1 v1.5: 55 | echo password | step-kms-plugin encrypt yubikey:slot-id=82 56 | 57 | # Encrypt a given file using RSA-OAEP: 58 | step-kms-plugin encrypt --oaep --in message.txt \ 59 | --kms 'pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM?pin-value=0001password' \ 60 | 'pkcs11:object=my-rsa-key' 61 | 62 | # Encrypt a given file using RSA-OAEP without an OAEP label: 63 | step-kms-plugin encrypt --oaep --in message.txt --no-label \ 64 | --kms 'cloudkms:' \ 65 | 'projects/my-project-id/locations/global/keyRings/my-decrypter-ring/cryptoKeys/my-decrypter/cryptoKeyVersions/1' 66 | 67 | # Encrypt a given file using RSA-OAEP and a custom label: 68 | step-kms-plugin encrypt --oaep --in message.txt --label my-custom-label \ 69 | --kms 'pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM?pin-value=0001password' \ 70 | 'pkcs11:object=my-rsa-key' 71 | 72 | # Encrypt a given file using a key in disk using the hexadecimal format: 73 | step-kms-plugin encrypt --format hex --in message.txt --kms softkms: rsa.pub 74 | 75 | # Encrypt a given file using an Attestation Key in the default TPM KMS: 76 | step kms encrypt --in message.txt 'tpmkms:my-ak;ak=true' 77 | 78 | # Encrypt a given file using a key in the default TPM KMS: 79 | step kms encrypt --in message.txt tpmkms:my-key`, 80 | RunE: func(cmd *cobra.Command, args []string) error { 81 | if l := len(args); l != 1 { 82 | return showErrUsage(cmd) 83 | } 84 | 85 | name := args[0] 86 | flags := cmd.Flags() 87 | kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms")) 88 | if kuri == "" { 89 | kuri = name 90 | } 91 | 92 | oaep := flagutil.MustBool(flags, "oaep") 93 | label := flagutil.MustString(flags, "label") 94 | noLabel := flagutil.MustBool(flags, "no-label") 95 | format := flagutil.MustString(flags, "format") 96 | in := flagutil.MustString(flags, "in") 97 | 98 | // OAEP requires a hash algorithm. 99 | // It uses SHA256 by default. 100 | var hash crypto.Hash 101 | var err error 102 | if oaep { 103 | alg := flagutil.MustString(flags, "alg") 104 | if hash, err = getHashAlgorithm(alg); err != nil { 105 | return err 106 | } 107 | } 108 | 109 | // Read input 110 | var data []byte 111 | if in != "" { 112 | if data, err = os.ReadFile(in); err != nil { 113 | return fmt.Errorf("failed to read file %q: %w", in, err) 114 | } 115 | } else { 116 | if data, err = io.ReadAll(os.Stdin); err != nil { 117 | return fmt.Errorf("failed to read from STDIN: %w", err) 118 | } 119 | } 120 | 121 | km, err := openKMS(cmd.Context(), kuri) 122 | if err != nil { 123 | return fmt.Errorf("failed to load key manager: %w", err) 124 | } 125 | defer km.Close() 126 | 127 | key, err := km.GetPublicKey(&apiv1.GetPublicKeyRequest{ 128 | Name: name, 129 | }) 130 | if err != nil { 131 | return fmt.Errorf("failed to get the public key: %w", err) 132 | } 133 | 134 | pub, ok := key.(*rsa.PublicKey) 135 | if !ok { 136 | return fmt.Errorf("%s is not an RSA key", kuri) 137 | } 138 | 139 | var b []byte 140 | if oaep { 141 | var oaepLabel []byte 142 | switch { 143 | case noLabel: 144 | break // nothing to do 145 | default: 146 | oaepLabel = []byte(label) 147 | } 148 | if b, err = rsa.EncryptOAEP(hash.New(), rand.Reader, pub, data, oaepLabel); err != nil { 149 | return err 150 | } 151 | } else { 152 | if b, err = rsa.EncryptPKCS1v15(rand.Reader, pub, data); err != nil { 153 | return err 154 | } 155 | } 156 | 157 | switch format { 158 | case "hex": 159 | fmt.Println(hex.EncodeToString(b)) 160 | case "raw": 161 | os.Stdout.Write(b) 162 | default: 163 | fmt.Println(base64.StdEncoding.EncodeToString(b)) 164 | } 165 | 166 | return nil 167 | }, 168 | } 169 | 170 | func getHashAlgorithm(alg string) (crypto.Hash, error) { 171 | switch alg { 172 | case "", "SHA256": 173 | return crypto.SHA256, nil 174 | case "SHA384": 175 | return crypto.SHA384, nil 176 | case "SHA512": 177 | return crypto.SHA512, nil 178 | default: 179 | return 0, fmt.Errorf("unsupported hashing algorithm %q", alg) 180 | } 181 | } 182 | 183 | func init() { 184 | rootCmd.AddCommand(encryptCmd) 185 | encryptCmd.SilenceUsage = true 186 | 187 | flags := encryptCmd.Flags() 188 | flags.SortFlags = false 189 | 190 | alg := flagutil.NormalizedValue("alg", []string{"SHA256", "SHA384", "SHA512"}, "SHA256") 191 | format := flagutil.LowerValue("format", []string{"base64", "hex", "raw"}, "base64") 192 | 193 | flags.Bool("oaep", false, "Use RSA-OAEP instead of RSA PKCS #1 v1.5") 194 | flags.Bool("no-label", false, "Omit setting the label when RSA-OAEP is used") 195 | flags.String("label", DefaultOEAPLabel, "Set a label when using RSA-OAEP") 196 | flags.Var(alg, "alg", "The hashing `algorithm` to use on RSA-OAEP.\nOptions are SHA256, SHA384 or SHA512") 197 | flags.Var(format, "format", "The `format` used in the input.\nOptions are base64, hex, or raw") 198 | flags.String("in", "", "The `file` to encrypt instead of using STDIN.") 199 | } 200 | -------------------------------------------------------------------------------- /cmd/fips.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build fips 16 | // +build fips 17 | 18 | package cmd 19 | 20 | import _ "crypto/tls/fipsonly" 21 | -------------------------------------------------------------------------------- /cmd/key.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "fmt" 18 | "io/fs" 19 | 20 | "github.com/spf13/cobra" 21 | "go.step.sm/crypto/kms" 22 | 23 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 24 | ) 25 | 26 | // keyCmd represents the key command 27 | var keyCmd = &cobra.Command{ 28 | Use: "key ", 29 | Short: "print the public key in a KMS", 30 | Long: `Prints a public key stored in a KMS.`, 31 | Example: ` # Get the public key defining the kms uri and key together: 32 | step-kms-plugin key \ 33 | "pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm;id=1000?pin-value=pass" 34 | 35 | # Get the public key using a PKCS #11 id: 36 | step-kms-plugin key \ 37 | --kms "pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass" \ 38 | "pkcs11:id=1000" 39 | 40 | # Get the public key using the PKCS #11 label: 41 | step-kms-plugin key \ 42 | --kms "pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-source=/var/run/pass.txt" \ 43 | "pkcs11:object=my-key" 44 | 45 | # Get the public key from Google's Cloud KMS: 46 | step-kms-plugin key \ 47 | cloudkms:projects/my-project/locations/us-west1/keyRings/my-keyring/cryptoKeys/my-rsa-key/cryptoKeyVersions/1 48 | 49 | # Get the public key from Azure's Key Vault: 50 | step-kms-plugin key 'azurekms:vault=my-key-vault;name=my-key' 51 | 52 | # Get the public key key from AWS KMS. 53 | step-kms-plugin key 'awskms:key-id=acbebc8f-822d-4c1c-b5d1-eb3a8fcaced7;region=us-west-1' 54 | 55 | # Get key from a YubiKey: 56 | step-kms-plugin key yubikey:slot-id=82 57 | 58 | # Get a key from the ssh-agent: 59 | step-kms-plugin key sshagentkms:user@localhost 60 | 61 | # Get a key from the default TPM KMS with KMS URI: 62 | step-kms-plugin key my-key --kms tpmkms 63 | 64 | # Get a key from the a TPM KMS, backed by /tmp/tpmobjects with KMS URI: 65 | step-kms-plugin key my-key --kms tpmkms:storage-directory=/tmp/tpmobjects 66 | 67 | # Get a key from the default TPM KMS: 68 | step-kms-plugin key tpmkms:name=my-key 69 | 70 | # Get a key from the TSS2 PEM file: 71 | step-kms-plugin key tpmkms:path=tss2.pem 72 | 73 | # Get an AK public key from the default TPM KMS: 74 | step-kms-plugin key 'tpmkms:name=my-ak;ak=true'`, 75 | RunE: func(cmd *cobra.Command, args []string) error { 76 | if len(args) != 1 { 77 | return showErrUsage(cmd) 78 | } 79 | 80 | flags := cmd.Flags() 81 | 82 | kuri, name, err := getURIAndNameForFS(flagutil.MustString(flags, "kms"), args[0]) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | fsys, err := kms.KeyFS(cmd.Context(), kuri) 88 | if err != nil { 89 | return err 90 | } 91 | defer fsys.Close() 92 | 93 | b, err := fs.ReadFile(fsys, name) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | fmt.Print(string(b)) 99 | return nil 100 | }, 101 | } 102 | 103 | func init() { 104 | rootCmd.AddCommand(keyCmd) 105 | keyCmd.SilenceUsage = true 106 | } 107 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "context" 18 | "crypto/x509" 19 | "encoding/pem" 20 | "errors" 21 | "fmt" 22 | "net/url" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/smallstep/cli-utils/step" 28 | "github.com/spf13/cobra" 29 | "go.step.sm/crypto/kms" 30 | "go.step.sm/crypto/kms/apiv1" 31 | "go.step.sm/crypto/kms/uri" 32 | "go.step.sm/crypto/pemutil" 33 | 34 | "github.com/smallstep/step-kms-plugin/internal/termutil" 35 | ) 36 | 37 | // rootCmd represents the base command when called without any subcommands 38 | var rootCmd = &cobra.Command{ 39 | Use: "step-kms-plugin", 40 | Short: "step plugin to manage KMSs", 41 | Long: `step-kms-plugin is a plugin for step that allows performing cryptographic 42 | operations on Hardware Security Modules (HSMs), Cloud Key Management Services 43 | (KMSs) and devices like YubiKey that implement a Personal Identity Verification (PIV) 44 | interface. This command uses the term KMS to refer to any of these interfaces. 45 | 46 | step-kms-plugin can be used using 'step kms [command]', or as a standalone 47 | application. 48 | 49 | Common operations include: 50 | - Create asymmetric key pair on a KMS 51 | - Sign data using an existing KMS-stored key 52 | - Extract public keys and certificates stored in a KMS`, 53 | } 54 | 55 | // Execute adds all child commands to the root command and sets flags appropriately. 56 | // This is called by main.main(). It only needs to happen once to the rootCmd. 57 | func Execute() { 58 | err := rootCmd.Execute() 59 | if err != nil { 60 | os.Exit(1) 61 | } 62 | } 63 | 64 | var errUsage = errors.New("usage") 65 | 66 | func showErrUsage(cmd *cobra.Command) error { 67 | cmd.SilenceErrors = true 68 | cmd.SilenceUsage = false 69 | return errUsage 70 | } 71 | 72 | // openKMS is a helper on top of kms.New that can set custom options depending 73 | // on the KMS type. 74 | func openKMS(ctx context.Context, kuri string) (apiv1.KeyManager, error) { 75 | typ, err := apiv1.TypeOf(kuri) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | var storageDirectory string 81 | if typ == apiv1.TPMKMS { 82 | if err := step.Init(); err != nil { 83 | return nil, err 84 | } 85 | storageDirectory = filepath.Join(step.Path(), "tpm") 86 | } 87 | 88 | // Type is not necessary, but it avoids an extra validation 89 | return kms.New(ctx, apiv1.Options{ 90 | Type: typ, 91 | URI: kuri, 92 | StorageDirectory: storageDirectory, 93 | }) 94 | } 95 | 96 | // changeURI adds extra parameters to the given uri. 97 | // 98 | // If the given values are already in the rawuri, the new values will take 99 | // preference. 100 | func changeURI(rawuri string, values url.Values) (string, error) { 101 | u, err := uri.Parse(rawuri) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | // Modify RawQuery with the given values 107 | v := u.Query() 108 | for k, vs := range values { 109 | for _, vv := range vs { 110 | v.Add(k, vv) 111 | } 112 | } 113 | u.RawQuery = v.Encode() 114 | 115 | return u.String(), nil 116 | } 117 | 118 | // ensureSchemePrefix checks if a (non-empty) KMS URI contains a 119 | // colon, indicating it contains a potentially valid KMS scheme. 120 | // If the KMS URI doesn't start with a scheme, the colon is suffixed. 121 | // This allows users to provide '--kms tpmkms' instead of requiring 122 | // '--kms tpmkms:', which results in a potentially confusing error 123 | // message. 124 | func ensureSchemePrefix(kuri string) string { 125 | if kuri != "" && !strings.Contains(kuri, ":") { 126 | kuri = fmt.Sprintf("%s:", kuri) 127 | } 128 | return kuri 129 | } 130 | 131 | // getURIAndNameForFS returns the kuri and name to be used by a KMS FS. If TPM 132 | // KMS is used, it changes the kuri to add the default storage directory. 133 | // 134 | // If a storage-directory is already in the kuri, this will take preference. 135 | func getURIAndNameForFS(kuri, name string) (string, string, error) { 136 | kuri = ensureSchemePrefix(kuri) 137 | if kuri == "" { 138 | kuri = name 139 | } 140 | 141 | typ, err := apiv1.TypeOf(kuri) 142 | if err != nil { 143 | return "", "", err 144 | } 145 | 146 | if typ == apiv1.TPMKMS { 147 | if err := step.Init(); err != nil { 148 | return "", "", err 149 | } 150 | kuri, err = changeURI(kuri, url.Values{"storage-directory": []string{filepath.Join(step.Path(), "tpm")}}) 151 | if err != nil { 152 | return "", "", err 153 | } 154 | } 155 | 156 | return kuri, name, nil 157 | } 158 | 159 | // outputCert encodes an X.509 certificate to PEM format 160 | // and writes it to stdout. 161 | func outputCert(c *x509.Certificate) error { 162 | if err := pem.Encode(os.Stdout, &pem.Block{ 163 | Type: "CERTIFICATE", 164 | Bytes: c.Raw, 165 | }); err != nil { 166 | return fmt.Errorf("failed to encode certificate: %w", err) 167 | } 168 | return nil 169 | } 170 | 171 | func init() { 172 | flags := rootCmd.PersistentFlags() 173 | flags.String("kms", "", "The `uri` with the kms configuration to use") 174 | 175 | // Define a password reader 176 | pemutil.PromptPassword = func(s string) ([]byte, error) { 177 | if s[len(s)-1] != ':' { 178 | s += ":" 179 | } 180 | return termutil.ReadPassword(s) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /cmd/sign.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "crypto" 18 | "crypto/ecdsa" 19 | "crypto/ed25519" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "encoding/base64" 24 | "encoding/hex" 25 | "errors" 26 | "fmt" 27 | "io" 28 | "math/big" 29 | "os" 30 | "strconv" 31 | "strings" 32 | 33 | "github.com/spf13/cobra" 34 | "go.step.sm/crypto/kms" 35 | "go.step.sm/crypto/kms/apiv1" 36 | "go.step.sm/crypto/kms/pkcs11" 37 | "go.step.sm/crypto/sshutil" 38 | "golang.org/x/crypto/cryptobyte" 39 | "golang.org/x/crypto/cryptobyte/asn1" 40 | "golang.org/x/crypto/ssh" 41 | "golang.org/x/crypto/ssh/agent" 42 | 43 | "github.com/smallstep/step-kms-plugin/internal/flagutil" 44 | ) 45 | 46 | // signCmd represents the sign command 47 | var signCmd = &cobra.Command{ 48 | Use: "sign []", 49 | Short: "sign the given digest using the kms", 50 | Long: `Signs a digest or a file using a key in the KMS. 51 | 52 | While RSA and EC signing schemes sign a SHA-2 digest of the data, Ed25519 signs 53 | the data itself. To accommodate either approach, this command accepts two formats 54 | of input to be signed: a hex digest as an optional parameter, 55 | or a binary data filename via the --in flag. 56 | 57 | If you use the --in flag with an EC or RSA key, this command will generate the 58 | digest of the data file for you.`, 59 | Example: ` # Sign the given file using a key in the PKCS #11 module. 60 | step-kms-plugin sign --in data.bin \ 61 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 62 | 'pkcs11:id=1000' 63 | 64 | # Sign a digest using a key in Google's Cloud KMS. 65 | step-kms-plugin sign 1b8de4254213f8c3f784b3da4611eaeec1e720e74b4357029f8271b4ef9e1c2c \ 66 | cloudkms:projects/my-project/locations/us-west1/keyRings/my-keyring/cryptoKeys/my-rsa-key/cryptoKeyVersions/1 67 | 68 | # Sign and verify using RSA PKCS #1 with SHA512: 69 | step-kms-plugin sign --in data.bin --verify --alg SHA512 \ 70 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 71 | 'pkcs11:object=my-rsa-key' 72 | 73 | # Sign a file using an Ed25519 key in the ssh-agent: 74 | step-kms-plugin sign --in data.bin sshagentkms:user@localhost 75 | 76 | # Sign the header and payload of a JWT to produce the signature: 77 | step-kms-plugin sign --in data.jwt --format jws \ 78 | --kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \ 79 | 'pkcs11:id=1000 80 | 81 | # Sign a file using a key in the default TPM KMS: 82 | step-kms-plugin sign --in data.bin tpmkms:name=my-key 83 | 84 | # Sign a file using a key in the default a TSS2 PEM: 85 | step-kms-plugin sign --in data.bin tpmkms:path=tss2.pem 86 | 87 | # Sign and verify using a key in the default TPM KMS: 88 | step-kms-plugin sign --in data.bin --verify tpmkms:name=my-key`, 89 | RunE: func(cmd *cobra.Command, args []string) error { 90 | if l := len(args); l != 1 && l != 2 { 91 | return showErrUsage(cmd) 92 | } 93 | 94 | name := args[0] 95 | flags := cmd.Flags() 96 | alg := flagutil.MustString(flags, "alg") 97 | pss := flagutil.MustBool(flags, "pss") 98 | format := flagutil.MustString(flags, "format") 99 | in := flagutil.MustString(flags, "in") 100 | verify := flagutil.MustBool(flags, "verify") 101 | 102 | var saltLength int 103 | switch s := strings.ToLower(flagutil.MustString(flags, "salt-length")); s { 104 | case "", "auto": 105 | saltLength = rsa.PSSSaltLengthAuto 106 | case "equal-hash", "hash": 107 | saltLength = rsa.PSSSaltLengthEqualsHash 108 | default: 109 | var err error 110 | if saltLength, err = strconv.Atoi(s); err != nil { 111 | return fmt.Errorf("failed to parse --salt-length=%q: %w", s, err) 112 | } 113 | if saltLength < rsa.PSSSaltLengthEqualsHash { 114 | return fmt.Errorf("flag --salt-length=%q is not valid: salt length cannot be negative", s) 115 | } 116 | } 117 | 118 | kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms")) 119 | if kuri == "" { 120 | kuri = name 121 | } 122 | 123 | km, err := openKMS(cmd.Context(), kuri) 124 | if err != nil { 125 | return fmt.Errorf("failed to load key manager: %w", err) 126 | } 127 | defer km.Close() 128 | 129 | signer, err := km.CreateSigner(&apiv1.CreateSignerRequest{ 130 | SigningKey: name, 131 | }) 132 | if err != nil { 133 | return fmt.Errorf("failed to create signer: %w", err) 134 | } 135 | 136 | pub := signer.Public() 137 | so, err := getSignerOptions(km, pub, alg, pss, saltLength) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | var digest []byte 143 | switch { 144 | case in != "": 145 | data, err := os.ReadFile(in) 146 | if err != nil { 147 | return fmt.Errorf("failed to read file %q: %w", in, err) 148 | } 149 | if signsRawInput(pub) { 150 | digest = data 151 | } else if hashFunc := so.HashFunc(); hashFunc != 0 { 152 | h := hashFunc.New() 153 | h.Write(data) 154 | digest = h.Sum(nil) 155 | } else { 156 | digest = data 157 | } 158 | case len(args) == 2: 159 | if signsRawInput(pub) { 160 | return fmt.Errorf("flag --in is required for type %T", pub) 161 | } 162 | digest, err = hex.DecodeString(args[1]) 163 | if err != nil { 164 | return fmt.Errorf("failed to decode digest: %w", err) 165 | } 166 | default: 167 | // Data passed by stdin is in binary form. 168 | digest, err = io.ReadAll(os.Stdin) 169 | if err != nil { 170 | return err 171 | } 172 | } 173 | 174 | sig, err := signer.Sign(rand.Reader, digest, so) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | if verify { 180 | if !verifySignature(signer, digest, sig, so) { 181 | return fmt.Errorf("failed to verify the signature") 182 | } 183 | } 184 | 185 | switch format { 186 | case "hex": 187 | fmt.Println(hex.EncodeToString(sig)) 188 | case "jws": 189 | if sig, err = jwsSignature(sig, pub); err != nil { 190 | return err 191 | } 192 | fmt.Println(base64.RawURLEncoding.EncodeToString(sig)) 193 | case "raw": 194 | os.Stdout.Write(sig) 195 | default: 196 | fmt.Println(base64.StdEncoding.EncodeToString(sig)) 197 | } 198 | 199 | return nil 200 | }, 201 | } 202 | 203 | func signsRawInput(pub crypto.PublicKey) bool { 204 | switch pub.(type) { 205 | case ed25519.PublicKey: 206 | return true 207 | case ssh.PublicKey: 208 | return true 209 | default: 210 | return false 211 | } 212 | } 213 | 214 | func jwsSignature(sig []byte, pub crypto.PublicKey) ([]byte, error) { 215 | ec, ok := pub.(*ecdsa.PublicKey) 216 | if !ok { 217 | return sig, nil 218 | } 219 | 220 | var r, s big.Int 221 | var inner cryptobyte.String 222 | input := cryptobyte.String(sig) 223 | if !input.ReadASN1(&inner, asn1.SEQUENCE) || 224 | !input.Empty() || 225 | !inner.ReadASN1Integer(&r) || 226 | !inner.ReadASN1Integer(&s) || 227 | !inner.Empty() { 228 | return nil, errors.New("failed decoding ASN.1 signature") 229 | } 230 | 231 | curveBits := ec.Curve.Params().BitSize 232 | keyBytes := curveBits / 8 233 | if curveBits%8 > 0 { 234 | keyBytes++ 235 | } 236 | 237 | // We serialize the outputs (r and s) into big-endian byte arrays and pad 238 | // them with zeros on the left to make sure the sizes work out. Both arrays 239 | // must be keyBytes long, and the output must be 2*keyBytes long. 240 | rBytes := r.Bytes() 241 | rBytesPadded := make([]byte, keyBytes) 242 | copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) 243 | 244 | sBytes := s.Bytes() 245 | sBytesPadded := make([]byte, keyBytes) 246 | copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) 247 | 248 | //nolint:makezero // we actually want the 0 bytes padding 249 | return append(rBytesPadded, sBytesPadded...), nil 250 | } 251 | 252 | func getSignerOptions(km kms.KeyManager, pub crypto.PublicKey, alg string, pss bool, saltLength int) (crypto.SignerOpts, error) { 253 | switch k := pub.(type) { 254 | case *ecdsa.PublicKey: 255 | switch k.Curve { 256 | case elliptic.P256(): 257 | return crypto.SHA256, nil 258 | case elliptic.P384(): 259 | return crypto.SHA384, nil 260 | case elliptic.P521(): 261 | return crypto.SHA512, nil 262 | default: 263 | return nil, fmt.Errorf("unsupported elliptic curve %q", k.Curve.Params().Name) 264 | } 265 | case *rsa.PublicKey: 266 | var h crypto.Hash 267 | switch alg { 268 | case "", "SHA256": 269 | h = crypto.SHA256 270 | case "SHA384": 271 | h = crypto.SHA384 272 | case "SHA512": 273 | h = crypto.SHA512 274 | default: 275 | return nil, fmt.Errorf("unsupported hashing algorithm %q", alg) 276 | } 277 | if pss { 278 | pssOptions := &rsa.PSSOptions{ 279 | Hash: h, 280 | SaltLength: saltLength, 281 | } 282 | // rsa.PSSSaltLengthAuto is not supported by crypto11. The salt 283 | // length here is the same used by Go when PSSSaltLengthAuto is 284 | // used. 285 | // 286 | // This can be fixed if 287 | // https://github.com/ThalesIgnite/crypto11/pull/96 gets merged. 288 | if _, ok := km.(*pkcs11.PKCS11); ok && saltLength == rsa.PSSSaltLengthAuto { 289 | pssOptions.SaltLength = (k.N.BitLen()-1+7)/8 - 2 - h.Size() 290 | } 291 | return pssOptions, nil 292 | } 293 | return h, nil 294 | case ed25519.PublicKey: 295 | return crypto.Hash(0), nil 296 | case *agent.Key, ssh.PublicKey: 297 | pk, err := sshutil.CryptoPublicKey(pub) 298 | if err != nil { 299 | return nil, err 300 | } 301 | return getSignerOptions(km, pk, alg, pss, saltLength) 302 | default: 303 | return nil, fmt.Errorf("unsupported public key type %T", pub) 304 | } 305 | } 306 | 307 | func verifySignature(signer crypto.Signer, data, sig []byte, so crypto.SignerOpts) bool { 308 | switch pub := signer.Public().(type) { 309 | case *ecdsa.PublicKey: 310 | return ecdsa.VerifyASN1(pub, data, sig) 311 | case *rsa.PublicKey: 312 | if pss, ok := so.(*rsa.PSSOptions); ok { 313 | return rsa.VerifyPSS(pub, so.HashFunc(), data, sig, pss) == nil 314 | } 315 | return rsa.VerifyPKCS1v15(pub, so.HashFunc(), data, sig) == nil 316 | case ed25519.PublicKey: 317 | return ed25519.Verify(pub, data, sig) 318 | case ssh.PublicKey: 319 | // Attempt to use the last signature if available. 320 | if s, ok := signer.(interface{ LastSignature() *ssh.Signature }); ok { 321 | if sshSig := s.LastSignature(); sshSig != nil { 322 | return pub.Verify(data, s.LastSignature()) == nil 323 | } 324 | } 325 | // Verify using the resulting signature. 326 | // It won't work with sk keys. 327 | return pub.Verify(data, &ssh.Signature{ 328 | Format: sshFormat(pub, so), 329 | Blob: sig, 330 | }) == nil 331 | default: 332 | return false 333 | } 334 | } 335 | 336 | func sshFormat(pub ssh.PublicKey, so crypto.SignerOpts) string { 337 | if pub.Type() == ssh.KeyAlgoRSA { 338 | switch so.HashFunc() { 339 | case crypto.SHA256: 340 | return ssh.KeyAlgoRSASHA256 341 | case crypto.SHA512: 342 | return ssh.KeyAlgoRSASHA512 343 | case crypto.SHA1: 344 | return ssh.KeyAlgoRSA 345 | } 346 | } 347 | return pub.Type() 348 | } 349 | 350 | func init() { 351 | rootCmd.AddCommand(signCmd) 352 | signCmd.SilenceUsage = true 353 | 354 | flags := signCmd.Flags() 355 | flags.SortFlags = false 356 | 357 | alg := flagutil.NormalizedValue("alg", []string{"SHA256", "SHA384", "SHA512"}, "SHA256") 358 | format := flagutil.LowerValue("format", []string{"base64", "hex", "jws", "raw"}, "base64") 359 | 360 | flags.Var(alg, "alg", "The hashing `algorithm` to use on RSA PKCS #1 and RSA-PSS signatures.\nOptions are SHA256, SHA384 or SHA512") 361 | flags.Bool("pss", false, "Use RSA-PSS signature scheme instead of RSA PKCS #1") 362 | flags.String("salt-length", "auto", "The salt length used in the RSA-PSS signature scheme.\nOptions are auto (0), equal-hash (-1) or a positive integer") 363 | flags.Var(format, "format", "The `format` to print the signature.\nOptions are base64, hex, jws, or raw") 364 | flags.String("in", "", "The `file` to sign. Required for Ed25519 keys.") 365 | flags.Bool("verify", false, "Verify the signature with the public key") 366 | } 367 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package cmd 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "runtime" 20 | "strings" 21 | "time" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var ( 27 | Version string 28 | ReleaseDate string 29 | ) 30 | 31 | // versionCmd represents the version command 32 | var versionCmd = &cobra.Command{ 33 | Use: "version", 34 | Short: "print the current version", 35 | Long: "Prints the current version.", 36 | Run: func(cmd *cobra.Command, _ []string) { 37 | if Version == "" { 38 | Version = "0000000-dev" 39 | } 40 | if ReleaseDate == "" { 41 | ReleaseDate = time.Now().UTC().Format("2006-01-02 15:04 MST") 42 | } 43 | 44 | if strings.Contains(os.Getenv("LANG"), "UTF-8") { 45 | fmt.Printf("🔐 %s/%s (%s/%s)\n", cmd.Parent().Name(), Version, runtime.GOOS, runtime.GOARCH) 46 | fmt.Printf(" Release Date: %s\n", ReleaseDate) 47 | } else { 48 | fmt.Printf("%s/%s (%s/%s)\n", cmd.Parent().Name(), Version, runtime.GOOS, runtime.GOARCH) 49 | fmt.Printf("Release Date: %s\n", ReleaseDate) 50 | } 51 | }, 52 | } 53 | 54 | func init() { 55 | rootCmd.AddCommand(versionCmd) 56 | } 57 | -------------------------------------------------------------------------------- /completions/bash_completion: -------------------------------------------------------------------------------- 1 | # bash completion V2 for step-kms-plugin -*- shell-script -*- 2 | 3 | __step-kms-plugin_debug() 4 | { 5 | if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then 6 | echo "$*" >> "${BASH_COMP_DEBUG_FILE}" 7 | fi 8 | } 9 | 10 | # Macs have bash3 for which the bash-completion package doesn't include 11 | # _init_completion. This is a minimal version of that function. 12 | __step-kms-plugin_init_completion() 13 | { 14 | COMPREPLY=() 15 | _get_comp_words_by_ref "$@" cur prev words cword 16 | } 17 | 18 | # This function calls the step-kms-plugin program to obtain the completion 19 | # results and the directive. It fills the 'out' and 'directive' vars. 20 | __step-kms-plugin_get_completion_results() { 21 | local requestComp lastParam lastChar args 22 | 23 | # Prepare the command to request completions for the program. 24 | # Calling ${words[0]} instead of directly step-kms-plugin allows to handle aliases 25 | args=("${words[@]:1}") 26 | requestComp="${words[0]} __complete ${args[*]}" 27 | 28 | lastParam=${words[$((${#words[@]}-1))]} 29 | lastChar=${lastParam:$((${#lastParam}-1)):1} 30 | __step-kms-plugin_debug "lastParam ${lastParam}, lastChar ${lastChar}" 31 | 32 | if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then 33 | # If the last parameter is complete (there is a space following it) 34 | # We add an extra empty parameter so we can indicate this to the go method. 35 | __step-kms-plugin_debug "Adding extra empty parameter" 36 | requestComp="${requestComp} ''" 37 | fi 38 | 39 | # When completing a flag with an = (e.g., step-kms-plugin -n=) 40 | # bash focuses on the part after the =, so we need to remove 41 | # the flag part from $cur 42 | if [[ "${cur}" == -*=* ]]; then 43 | cur="${cur#*=}" 44 | fi 45 | 46 | __step-kms-plugin_debug "Calling ${requestComp}" 47 | # Use eval to handle any environment variables and such 48 | out=$(eval "${requestComp}" 2>/dev/null) 49 | 50 | # Extract the directive integer at the very end of the output following a colon (:) 51 | directive=${out##*:} 52 | # Remove the directive 53 | out=${out%:*} 54 | if [ "${directive}" = "${out}" ]; then 55 | # There is not directive specified 56 | directive=0 57 | fi 58 | __step-kms-plugin_debug "The completion directive is: ${directive}" 59 | __step-kms-plugin_debug "The completions are: ${out}" 60 | } 61 | 62 | __step-kms-plugin_process_completion_results() { 63 | local shellCompDirectiveError=1 64 | local shellCompDirectiveNoSpace=2 65 | local shellCompDirectiveNoFileComp=4 66 | local shellCompDirectiveFilterFileExt=8 67 | local shellCompDirectiveFilterDirs=16 68 | 69 | if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 70 | # Error code. No completion. 71 | __step-kms-plugin_debug "Received error from custom completion go code" 72 | return 73 | else 74 | if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 75 | if [[ $(type -t compopt) = "builtin" ]]; then 76 | __step-kms-plugin_debug "Activating no space" 77 | compopt -o nospace 78 | else 79 | __step-kms-plugin_debug "No space directive not supported in this version of bash" 80 | fi 81 | fi 82 | if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 83 | if [[ $(type -t compopt) = "builtin" ]]; then 84 | __step-kms-plugin_debug "Activating no file completion" 85 | compopt +o default 86 | else 87 | __step-kms-plugin_debug "No file completion directive not supported in this version of bash" 88 | fi 89 | fi 90 | fi 91 | 92 | # Separate activeHelp from normal completions 93 | local completions=() 94 | local activeHelp=() 95 | __step-kms-plugin_extract_activeHelp 96 | 97 | if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 98 | # File extension filtering 99 | local fullFilter filter filteringCmd 100 | 101 | # Do not use quotes around the $completions variable or else newline 102 | # characters will be kept. 103 | for filter in ${completions[*]}; do 104 | fullFilter+="$filter|" 105 | done 106 | 107 | filteringCmd="_filedir $fullFilter" 108 | __step-kms-plugin_debug "File filtering command: $filteringCmd" 109 | $filteringCmd 110 | elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 111 | # File completion for directories only 112 | 113 | # Use printf to strip any trailing newline 114 | local subdir 115 | subdir=$(printf "%s" "${completions[0]}") 116 | if [ -n "$subdir" ]; then 117 | __step-kms-plugin_debug "Listing directories in $subdir" 118 | pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 119 | else 120 | __step-kms-plugin_debug "Listing directories in ." 121 | _filedir -d 122 | fi 123 | else 124 | __step-kms-plugin_handle_completion_types 125 | fi 126 | 127 | __step-kms-plugin_handle_special_char "$cur" : 128 | __step-kms-plugin_handle_special_char "$cur" = 129 | 130 | # Print the activeHelp statements before we finish 131 | if [ ${#activeHelp} -ne 0 ]; then 132 | printf "\n"; 133 | printf "%s\n" "${activeHelp[@]}" 134 | printf "\n" 135 | 136 | # The prompt format is only available from bash 4.4. 137 | # We test if it is available before using it. 138 | if (x=${PS1@P}) 2> /dev/null; then 139 | printf "%s" "${PS1@P}${COMP_LINE[@]}" 140 | else 141 | # Can't print the prompt. Just print the 142 | # text the user had typed, it is workable enough. 143 | printf "%s" "${COMP_LINE[@]}" 144 | fi 145 | fi 146 | } 147 | 148 | # Separate activeHelp lines from real completions. 149 | # Fills the $activeHelp and $completions arrays. 150 | __step-kms-plugin_extract_activeHelp() { 151 | local activeHelpMarker="_activeHelp_ " 152 | local endIndex=${#activeHelpMarker} 153 | 154 | while IFS='' read -r comp; do 155 | if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then 156 | comp=${comp:endIndex} 157 | __step-kms-plugin_debug "ActiveHelp found: $comp" 158 | if [ -n "$comp" ]; then 159 | activeHelp+=("$comp") 160 | fi 161 | else 162 | # Not an activeHelp line but a normal completion 163 | completions+=("$comp") 164 | fi 165 | done < <(printf "%s\n" "${out}") 166 | } 167 | 168 | __step-kms-plugin_handle_completion_types() { 169 | __step-kms-plugin_debug "__step-kms-plugin_handle_completion_types: COMP_TYPE is $COMP_TYPE" 170 | 171 | case $COMP_TYPE in 172 | 37|42) 173 | # Type: menu-complete/menu-complete-backward and insert-completions 174 | # If the user requested inserting one completion at a time, or all 175 | # completions at once on the command-line we must remove the descriptions. 176 | # https://github.com/spf13/cobra/issues/1508 177 | local tab=$'\t' comp 178 | while IFS='' read -r comp; do 179 | [[ -z $comp ]] && continue 180 | # Strip any description 181 | comp=${comp%%$tab*} 182 | # Only consider the completions that match 183 | if [[ $comp == "$cur"* ]]; then 184 | COMPREPLY+=("$comp") 185 | fi 186 | done < <(printf "%s\n" "${completions[@]}") 187 | ;; 188 | 189 | *) 190 | # Type: complete (normal completion) 191 | __step-kms-plugin_handle_standard_completion_case 192 | ;; 193 | esac 194 | } 195 | 196 | __step-kms-plugin_handle_standard_completion_case() { 197 | local tab=$'\t' comp 198 | 199 | # Short circuit to optimize if we don't have descriptions 200 | if [[ "${completions[*]}" != *$tab* ]]; then 201 | IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") 202 | return 0 203 | fi 204 | 205 | local longest=0 206 | local compline 207 | # Look for the longest completion so that we can format things nicely 208 | while IFS='' read -r compline; do 209 | [[ -z $compline ]] && continue 210 | # Strip any description before checking the length 211 | comp=${compline%%$tab*} 212 | # Only consider the completions that match 213 | [[ $comp == "$cur"* ]] || continue 214 | COMPREPLY+=("$compline") 215 | if ((${#comp}>longest)); then 216 | longest=${#comp} 217 | fi 218 | done < <(printf "%s\n" "${completions[@]}") 219 | 220 | # If there is a single completion left, remove the description text 221 | if [ ${#COMPREPLY[*]} -eq 1 ]; then 222 | __step-kms-plugin_debug "COMPREPLY[0]: ${COMPREPLY[0]}" 223 | comp="${COMPREPLY[0]%%$tab*}" 224 | __step-kms-plugin_debug "Removed description from single completion, which is now: ${comp}" 225 | COMPREPLY[0]=$comp 226 | else # Format the descriptions 227 | __step-kms-plugin_format_comp_descriptions $longest 228 | fi 229 | } 230 | 231 | __step-kms-plugin_handle_special_char() 232 | { 233 | local comp="$1" 234 | local char=$2 235 | if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then 236 | local word=${comp%"${comp##*${char}}"} 237 | local idx=${#COMPREPLY[*]} 238 | while [[ $((--idx)) -ge 0 ]]; do 239 | COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} 240 | done 241 | fi 242 | } 243 | 244 | __step-kms-plugin_format_comp_descriptions() 245 | { 246 | local tab=$'\t' 247 | local comp desc maxdesclength 248 | local longest=$1 249 | 250 | local i ci 251 | for ci in ${!COMPREPLY[*]}; do 252 | comp=${COMPREPLY[ci]} 253 | # Properly format the description string which follows a tab character if there is one 254 | if [[ "$comp" == *$tab* ]]; then 255 | __step-kms-plugin_debug "Original comp: $comp" 256 | desc=${comp#*$tab} 257 | comp=${comp%%$tab*} 258 | 259 | # $COLUMNS stores the current shell width. 260 | # Remove an extra 4 because we add 2 spaces and 2 parentheses. 261 | maxdesclength=$(( COLUMNS - longest - 4 )) 262 | 263 | # Make sure we can fit a description of at least 8 characters 264 | # if we are to align the descriptions. 265 | if [[ $maxdesclength -gt 8 ]]; then 266 | # Add the proper number of spaces to align the descriptions 267 | for ((i = ${#comp} ; i < longest ; i++)); do 268 | comp+=" " 269 | done 270 | else 271 | # Don't pad the descriptions so we can fit more text after the completion 272 | maxdesclength=$(( COLUMNS - ${#comp} - 4 )) 273 | fi 274 | 275 | # If there is enough space for any description text, 276 | # truncate the descriptions that are too long for the shell width 277 | if [ $maxdesclength -gt 0 ]; then 278 | if [ ${#desc} -gt $maxdesclength ]; then 279 | desc=${desc:0:$(( maxdesclength - 1 ))} 280 | desc+="…" 281 | fi 282 | comp+=" ($desc)" 283 | fi 284 | COMPREPLY[ci]=$comp 285 | __step-kms-plugin_debug "Final comp: $comp" 286 | fi 287 | done 288 | } 289 | 290 | __start_step-kms-plugin() 291 | { 292 | local cur prev words cword split 293 | 294 | COMPREPLY=() 295 | 296 | # Call _init_completion from the bash-completion package 297 | # to prepare the arguments properly 298 | if declare -F _init_completion >/dev/null 2>&1; then 299 | _init_completion -n "=:" || return 300 | else 301 | __step-kms-plugin_init_completion -n "=:" || return 302 | fi 303 | 304 | __step-kms-plugin_debug 305 | __step-kms-plugin_debug "========= starting completion logic ==========" 306 | __step-kms-plugin_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" 307 | 308 | # The user could have moved the cursor backwards on the command-line. 309 | # We need to trigger completion from the $cword location, so we need 310 | # to truncate the command-line ($words) up to the $cword location. 311 | words=("${words[@]:0:$cword+1}") 312 | __step-kms-plugin_debug "Truncated words[*]: ${words[*]}," 313 | 314 | local out directive 315 | __step-kms-plugin_get_completion_results 316 | __step-kms-plugin_process_completion_results 317 | } 318 | 319 | if [[ $(type -t compopt) = "builtin" ]]; then 320 | complete -o default -F __start_step-kms-plugin step-kms-plugin 321 | else 322 | complete -o default -o nospace -F __start_step-kms-plugin step-kms-plugin 323 | fi 324 | 325 | # ex: ts=4 sw=4 et filetype=sh 326 | -------------------------------------------------------------------------------- /completions/fish_completion: -------------------------------------------------------------------------------- 1 | # fish completion for step-kms-plugin -*- shell-script -*- 2 | 3 | function __step_kms_plugin_debug 4 | set -l file "$BASH_COMP_DEBUG_FILE" 5 | if test -n "$file" 6 | echo "$argv" >> $file 7 | end 8 | end 9 | 10 | function __step_kms_plugin_perform_completion 11 | __step_kms_plugin_debug "Starting __step_kms_plugin_perform_completion" 12 | 13 | # Extract all args except the last one 14 | set -l args (commandline -opc) 15 | # Extract the last arg and escape it in case it is a space 16 | set -l lastArg (string escape -- (commandline -ct)) 17 | 18 | __step_kms_plugin_debug "args: $args" 19 | __step_kms_plugin_debug "last arg: $lastArg" 20 | 21 | # Disable ActiveHelp which is not supported for fish shell 22 | set -l requestComp "STEP_KMS_PLUGIN_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" 23 | 24 | __step_kms_plugin_debug "Calling $requestComp" 25 | set -l results (eval $requestComp 2> /dev/null) 26 | 27 | # Some programs may output extra empty lines after the directive. 28 | # Let's ignore them or else it will break completion. 29 | # Ref: https://github.com/spf13/cobra/issues/1279 30 | for line in $results[-1..1] 31 | if test (string trim -- $line) = "" 32 | # Found an empty line, remove it 33 | set results $results[1..-2] 34 | else 35 | # Found non-empty line, we have our proper output 36 | break 37 | end 38 | end 39 | 40 | set -l comps $results[1..-2] 41 | set -l directiveLine $results[-1] 42 | 43 | # For Fish, when completing a flag with an = (e.g., -n=) 44 | # completions must be prefixed with the flag 45 | set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 46 | 47 | __step_kms_plugin_debug "Comps: $comps" 48 | __step_kms_plugin_debug "DirectiveLine: $directiveLine" 49 | __step_kms_plugin_debug "flagPrefix: $flagPrefix" 50 | 51 | for comp in $comps 52 | printf "%s%s\n" "$flagPrefix" "$comp" 53 | end 54 | 55 | printf "%s\n" "$directiveLine" 56 | end 57 | 58 | # This function does two things: 59 | # - Obtain the completions and store them in the global __step_kms_plugin_comp_results 60 | # - Return false if file completion should be performed 61 | function __step_kms_plugin_prepare_completions 62 | __step_kms_plugin_debug "" 63 | __step_kms_plugin_debug "========= starting completion logic ==========" 64 | 65 | # Start fresh 66 | set --erase __step_kms_plugin_comp_results 67 | 68 | set -l results (__step_kms_plugin_perform_completion) 69 | __step_kms_plugin_debug "Completion results: $results" 70 | 71 | if test -z "$results" 72 | __step_kms_plugin_debug "No completion, probably due to a failure" 73 | # Might as well do file completion, in case it helps 74 | return 1 75 | end 76 | 77 | set -l directive (string sub --start 2 $results[-1]) 78 | set --global __step_kms_plugin_comp_results $results[1..-2] 79 | 80 | __step_kms_plugin_debug "Completions are: $__step_kms_plugin_comp_results" 81 | __step_kms_plugin_debug "Directive is: $directive" 82 | 83 | set -l shellCompDirectiveError 1 84 | set -l shellCompDirectiveNoSpace 2 85 | set -l shellCompDirectiveNoFileComp 4 86 | set -l shellCompDirectiveFilterFileExt 8 87 | set -l shellCompDirectiveFilterDirs 16 88 | 89 | if test -z "$directive" 90 | set directive 0 91 | end 92 | 93 | set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) 94 | if test $compErr -eq 1 95 | __step_kms_plugin_debug "Received error directive: aborting." 96 | # Might as well do file completion, in case it helps 97 | return 1 98 | end 99 | 100 | set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) 101 | set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) 102 | if test $filefilter -eq 1; or test $dirfilter -eq 1 103 | __step_kms_plugin_debug "File extension filtering or directory filtering not supported" 104 | # Do full file completion instead 105 | return 1 106 | end 107 | 108 | set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) 109 | set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) 110 | 111 | __step_kms_plugin_debug "nospace: $nospace, nofiles: $nofiles" 112 | 113 | # If we want to prevent a space, or if file completion is NOT disabled, 114 | # we need to count the number of valid completions. 115 | # To do so, we will filter on prefix as the completions we have received 116 | # may not already be filtered so as to allow fish to match on different 117 | # criteria than the prefix. 118 | if test $nospace -ne 0; or test $nofiles -eq 0 119 | set -l prefix (commandline -t | string escape --style=regex) 120 | __step_kms_plugin_debug "prefix: $prefix" 121 | 122 | set -l completions (string match -r -- "^$prefix.*" $__step_kms_plugin_comp_results) 123 | set --global __step_kms_plugin_comp_results $completions 124 | __step_kms_plugin_debug "Filtered completions are: $__step_kms_plugin_comp_results" 125 | 126 | # Important not to quote the variable for count to work 127 | set -l numComps (count $__step_kms_plugin_comp_results) 128 | __step_kms_plugin_debug "numComps: $numComps" 129 | 130 | if test $numComps -eq 1; and test $nospace -ne 0 131 | # We must first split on \t to get rid of the descriptions to be 132 | # able to check what the actual completion will be. 133 | # We don't need descriptions anyway since there is only a single 134 | # real completion which the shell will expand immediately. 135 | set -l split (string split --max 1 \t $__step_kms_plugin_comp_results[1]) 136 | 137 | # Fish won't add a space if the completion ends with any 138 | # of the following characters: @=/:., 139 | set -l lastChar (string sub -s -1 -- $split) 140 | if not string match -r -q "[@=/:.,]" -- "$lastChar" 141 | # In other cases, to support the "nospace" directive we trick the shell 142 | # by outputting an extra, longer completion. 143 | __step_kms_plugin_debug "Adding second completion to perform nospace directive" 144 | set --global __step_kms_plugin_comp_results $split[1] $split[1]. 145 | __step_kms_plugin_debug "Completions are now: $__step_kms_plugin_comp_results" 146 | end 147 | end 148 | 149 | if test $numComps -eq 0; and test $nofiles -eq 0 150 | # To be consistent with bash and zsh, we only trigger file 151 | # completion when there are no other completions 152 | __step_kms_plugin_debug "Requesting file completion" 153 | return 1 154 | end 155 | end 156 | 157 | return 0 158 | end 159 | 160 | # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 161 | # so we can properly delete any completions provided by another script. 162 | # Only do this if the program can be found, or else fish may print some errors; besides, 163 | # the existing completions will only be loaded if the program can be found. 164 | if type -q "step-kms-plugin" 165 | # The space after the program name is essential to trigger completion for the program 166 | # and not completion of the program name itself. 167 | # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 168 | complete --do-complete "step-kms-plugin " > /dev/null 2>&1 169 | end 170 | 171 | # Remove any pre-existing completions for the program since we will be handling all of them. 172 | complete -c step-kms-plugin -e 173 | 174 | # The call to __step_kms_plugin_prepare_completions will setup __step_kms_plugin_comp_results 175 | # which provides the program's completion choices. 176 | complete -c step-kms-plugin -n '__step_kms_plugin_prepare_completions' -f -a '$__step_kms_plugin_comp_results' 177 | 178 | -------------------------------------------------------------------------------- /completions/powershell_completion: -------------------------------------------------------------------------------- 1 | # powershell completion for step-kms-plugin -*- shell-script -*- 2 | 3 | function __step-kms-plugin_debug { 4 | if ($env:BASH_COMP_DEBUG_FILE) { 5 | "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" 6 | } 7 | } 8 | 9 | filter __step-kms-plugin_escapeStringWithSpecialChars { 10 | $_ -replace '\s|#|@|\$|;|,|''|\{|\}|\(|\)|"|`|\||<|>|&','`$&' 11 | } 12 | 13 | Register-ArgumentCompleter -CommandName 'step-kms-plugin' -ScriptBlock { 14 | param( 15 | $WordToComplete, 16 | $CommandAst, 17 | $CursorPosition 18 | ) 19 | 20 | # Get the current command line and convert into a string 21 | $Command = $CommandAst.CommandElements 22 | $Command = "$Command" 23 | 24 | __step-kms-plugin_debug "" 25 | __step-kms-plugin_debug "========= starting completion logic ==========" 26 | __step-kms-plugin_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" 27 | 28 | # The user could have moved the cursor backwards on the command-line. 29 | # We need to trigger completion from the $CursorPosition location, so we need 30 | # to truncate the command-line ($Command) up to the $CursorPosition location. 31 | # Make sure the $Command is longer then the $CursorPosition before we truncate. 32 | # This happens because the $Command does not include the last space. 33 | if ($Command.Length -gt $CursorPosition) { 34 | $Command=$Command.Substring(0,$CursorPosition) 35 | } 36 | __step-kms-plugin_debug "Truncated command: $Command" 37 | 38 | $ShellCompDirectiveError=1 39 | $ShellCompDirectiveNoSpace=2 40 | $ShellCompDirectiveNoFileComp=4 41 | $ShellCompDirectiveFilterFileExt=8 42 | $ShellCompDirectiveFilterDirs=16 43 | 44 | # Prepare the command to request completions for the program. 45 | # Split the command at the first space to separate the program and arguments. 46 | $Program,$Arguments = $Command.Split(" ",2) 47 | 48 | $RequestComp="$Program __complete $Arguments" 49 | __step-kms-plugin_debug "RequestComp: $RequestComp" 50 | 51 | # we cannot use $WordToComplete because it 52 | # has the wrong values if the cursor was moved 53 | # so use the last argument 54 | if ($WordToComplete -ne "" ) { 55 | $WordToComplete = $Arguments.Split(" ")[-1] 56 | } 57 | __step-kms-plugin_debug "New WordToComplete: $WordToComplete" 58 | 59 | 60 | # Check for flag with equal sign 61 | $IsEqualFlag = ($WordToComplete -Like "--*=*" ) 62 | if ( $IsEqualFlag ) { 63 | __step-kms-plugin_debug "Completing equal sign flag" 64 | # Remove the flag part 65 | $Flag,$WordToComplete = $WordToComplete.Split("=",2) 66 | } 67 | 68 | if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { 69 | # If the last parameter is complete (there is a space following it) 70 | # We add an extra empty parameter so we can indicate this to the go method. 71 | __step-kms-plugin_debug "Adding extra empty parameter" 72 | # We need to use `"`" to pass an empty argument a "" or '' does not work!!! 73 | $RequestComp="$RequestComp" + ' `"`"' 74 | } 75 | 76 | __step-kms-plugin_debug "Calling $RequestComp" 77 | # First disable ActiveHelp which is not supported for Powershell 78 | $env:STEP_KMS_PLUGIN_ACTIVE_HELP=0 79 | 80 | #call the command store the output in $out and redirect stderr and stdout to null 81 | # $Out is an array contains each line per element 82 | Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null 83 | 84 | # get directive from last line 85 | [int]$Directive = $Out[-1].TrimStart(':') 86 | if ($Directive -eq "") { 87 | # There is no directive specified 88 | $Directive = 0 89 | } 90 | __step-kms-plugin_debug "The completion directive is: $Directive" 91 | 92 | # remove directive (last element) from out 93 | $Out = $Out | Where-Object { $_ -ne $Out[-1] } 94 | __step-kms-plugin_debug "The completions are: $Out" 95 | 96 | if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { 97 | # Error code. No completion. 98 | __step-kms-plugin_debug "Received error from custom completion go code" 99 | return 100 | } 101 | 102 | $Longest = 0 103 | $Values = $Out | ForEach-Object { 104 | #Split the output in name and description 105 | $Name, $Description = $_.Split("`t",2) 106 | __step-kms-plugin_debug "Name: $Name Description: $Description" 107 | 108 | # Look for the longest completion so that we can format things nicely 109 | if ($Longest -lt $Name.Length) { 110 | $Longest = $Name.Length 111 | } 112 | 113 | # Set the description to a one space string if there is none set. 114 | # This is needed because the CompletionResult does not accept an empty string as argument 115 | if (-Not $Description) { 116 | $Description = " " 117 | } 118 | @{Name="$Name";Description="$Description"} 119 | } 120 | 121 | 122 | $Space = " " 123 | if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { 124 | # remove the space here 125 | __step-kms-plugin_debug "ShellCompDirectiveNoSpace is called" 126 | $Space = "" 127 | } 128 | 129 | if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or 130 | (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { 131 | __step-kms-plugin_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" 132 | 133 | # return here to prevent the completion of the extensions 134 | return 135 | } 136 | 137 | $Values = $Values | Where-Object { 138 | # filter the result 139 | $_.Name -like "$WordToComplete*" 140 | 141 | # Join the flag back if we have an equal sign flag 142 | if ( $IsEqualFlag ) { 143 | __step-kms-plugin_debug "Join the equal sign flag back to the completion value" 144 | $_.Name = $Flag + "=" + $_.Name 145 | } 146 | } 147 | 148 | if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { 149 | __step-kms-plugin_debug "ShellCompDirectiveNoFileComp is called" 150 | 151 | if ($Values.Length -eq 0) { 152 | # Just print an empty string here so the 153 | # shell does not start to complete paths. 154 | # We cannot use CompletionResult here because 155 | # it does not accept an empty string as argument. 156 | "" 157 | return 158 | } 159 | } 160 | 161 | # Get the current mode 162 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 163 | __step-kms-plugin_debug "Mode: $Mode" 164 | 165 | $Values | ForEach-Object { 166 | 167 | # store temporary because switch will overwrite $_ 168 | $comp = $_ 169 | 170 | # PowerShell supports three different completion modes 171 | # - TabCompleteNext (default windows style - on each key press the next option is displayed) 172 | # - Complete (works like bash) 173 | # - MenuComplete (works like zsh) 174 | # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function 175 | 176 | # CompletionResult Arguments: 177 | # 1) CompletionText text to be used as the auto completion result 178 | # 2) ListItemText text to be displayed in the suggestion list 179 | # 3) ResultType type of completion result 180 | # 4) ToolTip text for the tooltip with details about the object 181 | 182 | switch ($Mode) { 183 | 184 | # bash like 185 | "Complete" { 186 | 187 | if ($Values.Length -eq 1) { 188 | __step-kms-plugin_debug "Only one completion left" 189 | 190 | # insert space after value 191 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __step-kms-plugin_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 192 | 193 | } else { 194 | # Add the proper number of spaces to align the descriptions 195 | while($comp.Name.Length -lt $Longest) { 196 | $comp.Name = $comp.Name + " " 197 | } 198 | 199 | # Check for empty description and only add parentheses if needed 200 | if ($($comp.Description) -eq " " ) { 201 | $Description = "" 202 | } else { 203 | $Description = " ($($comp.Description))" 204 | } 205 | 206 | [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") 207 | } 208 | } 209 | 210 | # zsh like 211 | "MenuComplete" { 212 | # insert space after value 213 | # MenuComplete will automatically show the ToolTip of 214 | # the highlighted value at the bottom of the suggestions. 215 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __step-kms-plugin_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 216 | } 217 | 218 | # TabCompleteNext and in case we get something unknown 219 | Default { 220 | # Like MenuComplete but we don't want to add a space here because 221 | # the user need to press space anyway to get the completion. 222 | # Description will not be shown because that's not possible with TabCompleteNext 223 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __step-kms-plugin_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 224 | } 225 | } 226 | 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /completions/zsh_completion: -------------------------------------------------------------------------------- 1 | #compdef step-kms-plugin 2 | 3 | # zsh completion for step-kms-plugin -*- shell-script -*- 4 | 5 | __step-kms-plugin_debug() 6 | { 7 | local file="$BASH_COMP_DEBUG_FILE" 8 | if [[ -n ${file} ]]; then 9 | echo "$*" >> "${file}" 10 | fi 11 | } 12 | 13 | _step-kms-plugin() 14 | { 15 | local shellCompDirectiveError=1 16 | local shellCompDirectiveNoSpace=2 17 | local shellCompDirectiveNoFileComp=4 18 | local shellCompDirectiveFilterFileExt=8 19 | local shellCompDirectiveFilterDirs=16 20 | 21 | local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace 22 | local -a completions 23 | 24 | __step-kms-plugin_debug "\n========= starting completion logic ==========" 25 | __step-kms-plugin_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 26 | 27 | # The user could have moved the cursor backwards on the command-line. 28 | # We need to trigger completion from the $CURRENT location, so we need 29 | # to truncate the command-line ($words) up to the $CURRENT location. 30 | # (We cannot use $CURSOR as its value does not work when a command is an alias.) 31 | words=("${=words[1,CURRENT]}") 32 | __step-kms-plugin_debug "Truncated words[*]: ${words[*]}," 33 | 34 | lastParam=${words[-1]} 35 | lastChar=${lastParam[-1]} 36 | __step-kms-plugin_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 37 | 38 | # For zsh, when completing a flag with an = (e.g., step-kms-plugin -n=) 39 | # completions must be prefixed with the flag 40 | setopt local_options BASH_REMATCH 41 | if [[ "${lastParam}" =~ '-.*=' ]]; then 42 | # We are dealing with a flag with an = 43 | flagPrefix="-P ${BASH_REMATCH}" 44 | fi 45 | 46 | # Prepare the command to obtain completions 47 | requestComp="${words[1]} __complete ${words[2,-1]}" 48 | if [ "${lastChar}" = "" ]; then 49 | # If the last parameter is complete (there is a space following it) 50 | # We add an extra empty parameter so we can indicate this to the go completion code. 51 | __step-kms-plugin_debug "Adding extra empty parameter" 52 | requestComp="${requestComp} \"\"" 53 | fi 54 | 55 | __step-kms-plugin_debug "About to call: eval ${requestComp}" 56 | 57 | # Use eval to handle any environment variables and such 58 | out=$(eval ${requestComp} 2>/dev/null) 59 | __step-kms-plugin_debug "completion output: ${out}" 60 | 61 | # Extract the directive integer following a : from the last line 62 | local lastLine 63 | while IFS='\n' read -r line; do 64 | lastLine=${line} 65 | done < <(printf "%s\n" "${out[@]}") 66 | __step-kms-plugin_debug "last line: ${lastLine}" 67 | 68 | if [ "${lastLine[1]}" = : ]; then 69 | directive=${lastLine[2,-1]} 70 | # Remove the directive including the : and the newline 71 | local suffix 72 | (( suffix=${#lastLine}+2)) 73 | out=${out[1,-$suffix]} 74 | else 75 | # There is no directive specified. Leave $out as is. 76 | __step-kms-plugin_debug "No directive found. Setting do default" 77 | directive=0 78 | fi 79 | 80 | __step-kms-plugin_debug "directive: ${directive}" 81 | __step-kms-plugin_debug "completions: ${out}" 82 | __step-kms-plugin_debug "flagPrefix: ${flagPrefix}" 83 | 84 | if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 85 | __step-kms-plugin_debug "Completion received error. Ignoring completions." 86 | return 87 | fi 88 | 89 | local activeHelpMarker="_activeHelp_ " 90 | local endIndex=${#activeHelpMarker} 91 | local startIndex=$((${#activeHelpMarker}+1)) 92 | local hasActiveHelp=0 93 | while IFS='\n' read -r comp; do 94 | # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 95 | if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 96 | __step-kms-plugin_debug "ActiveHelp found: $comp" 97 | comp="${comp[$startIndex,-1]}" 98 | if [ -n "$comp" ]; then 99 | compadd -x "${comp}" 100 | __step-kms-plugin_debug "ActiveHelp will need delimiter" 101 | hasActiveHelp=1 102 | fi 103 | 104 | continue 105 | fi 106 | 107 | if [ -n "$comp" ]; then 108 | # If requested, completions are returned with a description. 109 | # The description is preceded by a TAB character. 110 | # For zsh's _describe, we need to use a : instead of a TAB. 111 | # We first need to escape any : as part of the completion itself. 112 | comp=${comp//:/\\:} 113 | 114 | local tab="$(printf '\t')" 115 | comp=${comp//$tab/:} 116 | 117 | __step-kms-plugin_debug "Adding completion: ${comp}" 118 | completions+=${comp} 119 | lastComp=$comp 120 | fi 121 | done < <(printf "%s\n" "${out[@]}") 122 | 123 | # Add a delimiter after the activeHelp statements, but only if: 124 | # - there are completions following the activeHelp statements, or 125 | # - file completion will be performed (so there will be choices after the activeHelp) 126 | if [ $hasActiveHelp -eq 1 ]; then 127 | if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 128 | __step-kms-plugin_debug "Adding activeHelp delimiter" 129 | compadd -x "--" 130 | hasActiveHelp=0 131 | fi 132 | fi 133 | 134 | if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 135 | __step-kms-plugin_debug "Activating nospace." 136 | noSpace="-S ''" 137 | fi 138 | 139 | if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 140 | # File extension filtering 141 | local filteringCmd 142 | filteringCmd='_files' 143 | for filter in ${completions[@]}; do 144 | if [ ${filter[1]} != '*' ]; then 145 | # zsh requires a glob pattern to do file filtering 146 | filter="\*.$filter" 147 | fi 148 | filteringCmd+=" -g $filter" 149 | done 150 | filteringCmd+=" ${flagPrefix}" 151 | 152 | __step-kms-plugin_debug "File filtering command: $filteringCmd" 153 | _arguments '*:filename:'"$filteringCmd" 154 | elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 155 | # File completion for directories only 156 | local subdir 157 | subdir="${completions[1]}" 158 | if [ -n "$subdir" ]; then 159 | __step-kms-plugin_debug "Listing directories in $subdir" 160 | pushd "${subdir}" >/dev/null 2>&1 161 | else 162 | __step-kms-plugin_debug "Listing directories in ." 163 | fi 164 | 165 | local result 166 | _arguments '*:dirname:_files -/'" ${flagPrefix}" 167 | result=$? 168 | if [ -n "$subdir" ]; then 169 | popd >/dev/null 2>&1 170 | fi 171 | return $result 172 | else 173 | __step-kms-plugin_debug "Calling _describe" 174 | if eval _describe "completions" completions $flagPrefix $noSpace; then 175 | __step-kms-plugin_debug "_describe found some completions" 176 | 177 | # Return the success of having called _describe 178 | return 0 179 | else 180 | __step-kms-plugin_debug "_describe did not find completions." 181 | __step-kms-plugin_debug "Checking if we should do file completion." 182 | if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 183 | __step-kms-plugin_debug "deactivating file completion" 184 | 185 | # We must return an error code here to let zsh know that there were no 186 | # completions found by _describe; this is what will trigger other 187 | # matching algorithms to attempt to find completions. 188 | # For example zsh can match letters in the middle of words. 189 | return 1 190 | else 191 | # Perform file completion 192 | __step-kms-plugin_debug "Activating file completion" 193 | 194 | # We must return the result of this command, so it must be the 195 | # last command, or else we must store its result to return it. 196 | _arguments '*:filename:_files'" ${flagPrefix}" 197 | fi 198 | fi 199 | fi 200 | } 201 | 202 | # don't run the completion function when being source-ed or eval-ed 203 | if [ "$funcstack[1]" = "_step-kms-plugin" ]; then 204 | _step-kms-plugin 205 | fi 206 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | WORKDIR /src 4 | COPY . . 5 | 6 | RUN apk add --no-cache git make 7 | RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev 8 | RUN make V=1 build 9 | 10 | FROM smallstep/step-cli:latest 11 | 12 | COPY --from=builder /src/bin/step-kms-plugin /usr/local/bin/step-kms-plugin 13 | 14 | USER root 15 | RUN apk add --no-cache pcsc-lite pcsc-lite-libs 16 | USER step 17 | 18 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /docker/Dockerfile.cloud: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | WORKDIR /src 4 | COPY . . 5 | 6 | RUN apk add --no-cache git make 7 | RUN make V=1 CGO_ENABLED=0 build 8 | 9 | FROM smallstep/step-cli:latest 10 | 11 | COPY --from=builder /src/bin/step-kms-plugin /usr/local/bin/step-kms-plugin 12 | 13 | USER step 14 | 15 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM golang:bookworm AS builder 2 | 3 | WORKDIR /src 4 | COPY . . 5 | 6 | RUN apt-get update 7 | RUN apt-get install -y --no-install-recommends gcc pkgconf libpcsclite-dev 8 | RUN make V=1 build 9 | 10 | FROM smallstep/step-cli:bookworm 11 | 12 | COPY --from=builder /src/bin/step-kms-plugin /usr/local/bin/step-kms-plugin 13 | 14 | USER root 15 | RUN apt-get update 16 | RUN apt-get install -y --no-install-recommends pcscd libpcsclite1 p11-kit p11-kit-modules 17 | USER step 18 | 19 | CMD ["/bin/bash"] 20 | -------------------------------------------------------------------------------- /docker/Dockerfile.wolfi: -------------------------------------------------------------------------------- 1 | FROM cgr.dev/chainguard/wolfi-base:latest AS builder 2 | 3 | WORKDIR /src 4 | COPY . . 5 | 6 | RUN apk update 7 | RUN apk add git make pkgconf gcc go 8 | RUN make V=1 build-fips 9 | 10 | FROM cgr.dev/chainguard/wolfi-base:latest 11 | 12 | COPY --from=builder /src/bin/step-kms-plugin /usr/bin/step-kms-plugin 13 | 14 | USER root 15 | RUN apk update 16 | RUN apk add p11-kit 17 | 18 | CMD ["/bin/sh"] 19 | -------------------------------------------------------------------------------- /docker/build/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | apt update 6 | apt install --no-install-recommends -y curl pkg-config libpcsclite-dev libpcsclite-dev:arm64 7 | 8 | # Install syft 9 | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin 10 | 11 | exec /entrypoint.sh $@ 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smallstep/step-kms-plugin 2 | 3 | go 1.23.6 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/fxamacker/cbor/v2 v2.8.0 9 | github.com/smallstep/cli-utils v0.12.1 10 | github.com/spf13/cobra v1.9.1 11 | github.com/spf13/pflag v1.0.6 12 | go.step.sm/crypto v0.64.0 13 | golang.org/x/crypto v0.38.0 14 | golang.org/x/term v0.32.0 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.120.0 // indirect 19 | cloud.google.com/go/auth v0.16.1 // indirect 20 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 21 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 22 | cloud.google.com/go/iam v1.5.0 // indirect 23 | cloud.google.com/go/kms v1.21.2 // indirect 24 | cloud.google.com/go/longrunning v0.6.6 // indirect 25 | dario.cat/mergo v1.0.1 // indirect 26 | filippo.io/edwards25519 v1.1.0 // indirect 27 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect 28 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect 29 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 30 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect 31 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect 32 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 33 | github.com/Masterminds/goutils v1.1.1 // indirect 34 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 35 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 36 | github.com/ThalesIgnite/crypto11 v1.2.5 // indirect 37 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 38 | github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect 39 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 40 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 41 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 42 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 43 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 44 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 45 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 46 | github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 // indirect 47 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 48 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 49 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 50 | github.com/aws/smithy-go v1.22.2 // indirect 51 | github.com/chzyer/readline v1.5.1 // indirect 52 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 53 | github.com/felixge/httpsnoop v1.0.4 // indirect 54 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 55 | github.com/go-logr/logr v1.4.2 // indirect 56 | github.com/go-logr/stdr v1.2.2 // indirect 57 | github.com/go-piv/piv-go/v2 v2.3.0 // indirect 58 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 59 | github.com/google/btree v1.1.2 // indirect 60 | github.com/google/certificate-transparency-go v1.1.6 // indirect 61 | github.com/google/go-tpm v0.9.5 // indirect 62 | github.com/google/go-tpm-tools v0.4.5 // indirect 63 | github.com/google/go-tspi v0.3.0 // indirect 64 | github.com/google/s2a-go v0.1.9 // indirect 65 | github.com/google/uuid v1.6.0 // indirect 66 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 67 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 68 | github.com/huandu/xstrings v1.5.0 // indirect 69 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 70 | github.com/kylelemons/godebug v1.1.0 // indirect 71 | github.com/manifoldco/promptui v0.9.0 // indirect 72 | github.com/miekg/pkcs11 v1.1.1 // indirect 73 | github.com/mitchellh/copystructure v1.2.0 // indirect 74 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 75 | github.com/peterbourgon/diskv/v3 v3.0.1 // indirect 76 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 77 | github.com/pkg/errors v0.9.1 // indirect 78 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 79 | github.com/schollz/jsonstore v1.1.0 // indirect 80 | github.com/shopspring/decimal v1.4.0 // indirect 81 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect 82 | github.com/spf13/cast v1.7.0 // indirect 83 | github.com/thales-e-security/pool v0.0.2 // indirect 84 | github.com/urfave/cli v1.22.16 // indirect 85 | github.com/x448/float16 v0.8.4 // indirect 86 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 87 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 88 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 89 | go.opentelemetry.io/otel v1.35.0 // indirect 90 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 91 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 92 | golang.org/x/net v0.40.0 // indirect 93 | golang.org/x/oauth2 v0.30.0 // indirect 94 | golang.org/x/sync v0.14.0 // indirect 95 | golang.org/x/sys v0.33.0 // indirect 96 | golang.org/x/text v0.25.0 // indirect 97 | golang.org/x/time v0.11.0 // indirect 98 | google.golang.org/api v0.232.0 // indirect 99 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect 100 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect 101 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect 102 | google.golang.org/grpc v1.72.0 // indirect 103 | google.golang.org/protobuf v1.36.6 // indirect 104 | ) 105 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= 2 | cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= 3 | cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= 4 | cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 7 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 8 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 9 | cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= 10 | cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= 11 | cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE= 12 | cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= 13 | cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= 14 | cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= 15 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 16 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 17 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 18 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 19 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 20 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 21 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= 22 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= 23 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= 24 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= 25 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= 26 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 27 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM= 28 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= 29 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= 30 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= 31 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= 32 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= 33 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 34 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 35 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 36 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 37 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 38 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 39 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 40 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 41 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 42 | github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= 43 | github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= 44 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 45 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 46 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 47 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 48 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 49 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 50 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 51 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 52 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 53 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 54 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 55 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 56 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 57 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 58 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 59 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 60 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 61 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 62 | github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= 63 | github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= 64 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 65 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 66 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 67 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 68 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 69 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 70 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 71 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 72 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 73 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 74 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 75 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 76 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 77 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 78 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 79 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 80 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 81 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 82 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 83 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 84 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 85 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 86 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 87 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 88 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 89 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 90 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 91 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 92 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 93 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 94 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 95 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 96 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 97 | github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 98 | github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 99 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 100 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 101 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 102 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 103 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 104 | github.com/go-piv/piv-go/v2 v2.3.0 h1:kKkrYlgLQTMPA6BiSL25A7/x4CEh2YCG7rtb/aTkx+g= 105 | github.com/go-piv/piv-go/v2 v2.3.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI= 106 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 107 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 108 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 109 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 110 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 111 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 112 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 113 | github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= 114 | github.com/google/certificate-transparency-go v1.1.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY= 115 | github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ= 116 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 117 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 118 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 119 | github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU= 120 | github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= 121 | github.com/google/go-sev-guest v0.12.1 h1:H4rFYnPIn8HtqEsNTmh56Zxcf9BI9n48ZSYCnpYLYvc= 122 | github.com/google/go-sev-guest v0.12.1/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760= 123 | github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A= 124 | github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= 125 | github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= 126 | github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= 127 | github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= 128 | github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= 129 | github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= 130 | github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 131 | github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= 132 | github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= 133 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 134 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 135 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 136 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 137 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 138 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 139 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 140 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 141 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 142 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 143 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 144 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 145 | github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= 146 | github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= 147 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 148 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 149 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 150 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 151 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 152 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 153 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 154 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 155 | github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 156 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 157 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 158 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 159 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 160 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 161 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 162 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 163 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 164 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 165 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 166 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 167 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 168 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 169 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 170 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 171 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 172 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 173 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 174 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 175 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 176 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 177 | github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= 178 | github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= 179 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 180 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 181 | github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= 182 | github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= 183 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= 184 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= 185 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 186 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 187 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 188 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 189 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 190 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 191 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 192 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 193 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 194 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 195 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 196 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 197 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 198 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 199 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 200 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 201 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 202 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 203 | github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= 204 | github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= 205 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 206 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 207 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 208 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 209 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 210 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 211 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 212 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 213 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 214 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 215 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 216 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 217 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 218 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 219 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 220 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 221 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 222 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 223 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 224 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 225 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 226 | go.step.sm/crypto v0.64.0 h1:tZ2k9Am6v3Y7cZCn89uTt77BYYXqvw+5WekUX3WZiXQ= 227 | go.step.sm/crypto v0.64.0/go.mod h1:EEY+UgKKqsvydv4mvtSpW2fqu2ezvPcAzkC80DwxmrI= 228 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 229 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 230 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 231 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 232 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 233 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 234 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 235 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 236 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 237 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 238 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 239 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 241 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 242 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 243 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 244 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 245 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 246 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 247 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 248 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 250 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 251 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 252 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 253 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 254 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 261 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 264 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 265 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 266 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 267 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 268 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 269 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 270 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 271 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 272 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 273 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 274 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 275 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 276 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 277 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 278 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 279 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 280 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 281 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 282 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 283 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 284 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 285 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 286 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 287 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 | google.golang.org/api v0.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I= 289 | google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= 290 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= 291 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= 292 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY= 293 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= 294 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= 295 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 296 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 297 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 298 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 299 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 300 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 301 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 302 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 303 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 304 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 305 | -------------------------------------------------------------------------------- /internal/flagutil/flagutil.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | // MustString returns the string value of a flag with the given name, it will 11 | // panic if the flag does not exists or is not the desired type. 12 | func MustString(flagSet *pflag.FlagSet, name string) string { 13 | v, err := flagSet.GetString(name) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return v 18 | } 19 | 20 | // MustInt returns the int value of a flag with the given name, it will panic if 21 | // the flag does not exists or is not the desired type. 22 | func MustInt(flagSet *pflag.FlagSet, name string) int { 23 | v, err := flagSet.GetInt(name) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return v 28 | } 29 | 30 | // MustBool returns the bool value of a flag with the given name, it will panic 31 | // if the flag does not exists or is not the desired type. 32 | func MustBool(flagSet *pflag.FlagSet, name string) bool { 33 | v, err := flagSet.GetBool(name) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return v 38 | } 39 | 40 | type value struct { 41 | Name string 42 | Allowed []string 43 | Value string 44 | Normalize func(string) string 45 | } 46 | 47 | // Value returns a pflag.Value interface with that will only accept values from 48 | // a given list. 49 | func Value(name string, allowed []string, defaultValue string) pflag.Value { 50 | return &value{ 51 | Name: name, 52 | Allowed: allowed, 53 | Value: defaultValue, 54 | } 55 | } 56 | 57 | // UpperValue returns a pflag.Value interface with that will only accept 58 | // values from a given list, but before checking the allowed list it will 59 | // normalized values to upper-case. 60 | func UpperValue(name string, allowed []string, defaultValue string) pflag.Value { 61 | return &value{ 62 | Name: name, 63 | Allowed: allowed, 64 | Value: defaultValue, 65 | Normalize: strings.ToUpper, 66 | } 67 | } 68 | 69 | // LowerValue returns a pflag.Value interface with that will only accept 70 | // values from a given list, but before checking the allowed list it will 71 | // normalized values to lower-case. 72 | func LowerValue(name string, allowed []string, defaultValue string) pflag.Value { 73 | return &value{ 74 | Name: name, 75 | Allowed: allowed, 76 | Value: defaultValue, 77 | Normalize: strings.ToLower, 78 | } 79 | } 80 | 81 | // NormalizedValue returns a pflag.Value interface with that will only accept 82 | // values from a given list, but before checking the allowed list it will 83 | // normalized values to upper-case and remove any dash or hyphens. 84 | func NormalizedValue(name string, allowed []string, defaultValue string) pflag.Value { 85 | return &value{ 86 | Name: name, 87 | Allowed: allowed, 88 | Value: defaultValue, 89 | Normalize: func(s string) string { 90 | s = strings.ReplaceAll(strings.ToUpper(s), "-", "") // Dash, hyphen-minus (-) 91 | s = strings.ReplaceAll(s, "\u2010", "") // Hyphen (‐) 92 | s = strings.ReplaceAll(s, "\u2011", "") // Non breaking hyphen (‑) 93 | s = strings.ReplaceAll(s, "\u2012", "") // Figure Dash (‒) 94 | s = strings.ReplaceAll(s, "\u2013", "") // En dash (–) 95 | return strings.ReplaceAll(s, "\u2014", "") // Em dash (—) 96 | }, 97 | } 98 | } 99 | 100 | func (v *value) String() string { 101 | return v.Value 102 | } 103 | 104 | func (v *value) Set(s string) error { 105 | if v.Normalize != nil { 106 | s = v.Normalize(s) 107 | } 108 | for _, a := range v.Allowed { 109 | if a == s { 110 | v.Value = a 111 | return nil 112 | } 113 | } 114 | return fmt.Errorf("value for flag --%s is not valid, options are %s", v.Name, strings.Join(v.Allowed, ",")) 115 | } 116 | 117 | func (v *value) Type() string { 118 | return "string" 119 | } 120 | -------------------------------------------------------------------------------- /internal/termutil/tui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The age Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //nolint:gocritic // this file is borrowed from age 6 | package termutil 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "os" 12 | "runtime" 13 | 14 | "golang.org/x/term" 15 | ) 16 | 17 | // clearLine clears the current line on the terminal, or opens a new line if 18 | // terminal escape codes don't work. 19 | func clearLine(out io.Writer) { 20 | const ( 21 | CUI = "\033[" // Control Sequence Introducer 22 | CPL = CUI + "F" // Cursor Previous Line 23 | EL = CUI + "K" // Erase in Line 24 | ) 25 | 26 | // First, open a new line, which is guaranteed to work everywhere. Then, try 27 | // to erase the line above with escape codes. 28 | // 29 | // (We use CRLF instead of LF to work around an apparent bug in WSL2's 30 | // handling of CONOUT$. Only when running a Windows binary from WSL2, the 31 | // cursor would not go back to the start of the line with a simple LF. 32 | // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.) 33 | fmt.Fprintf(out, "\r\n"+CPL+EL) 34 | } 35 | 36 | // withTerminal runs f with the terminal input and output files, if available. 37 | // withTerminal does not open a non-terminal stdin, so the caller does not need 38 | // to check stdinInUse. 39 | func withTerminal(f func(in, out *os.File) error) error { 40 | if runtime.GOOS == "windows" { 41 | in, err := os.OpenFile("CONIN$", os.O_RDWR, 0) 42 | if err != nil { 43 | return err 44 | } 45 | defer in.Close() 46 | out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0) 47 | if err != nil { 48 | return err 49 | } 50 | defer out.Close() 51 | return f(in, out) 52 | } 53 | 54 | var ( 55 | tty *os.File 56 | err error 57 | ) 58 | if tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { 59 | defer tty.Close() 60 | return f(tty, tty) 61 | } 62 | 63 | if term.IsTerminal(int(os.Stdin.Fd())) { 64 | return f(os.Stdin, os.Stdin) 65 | } 66 | 67 | return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %w", err) 68 | } 69 | 70 | // ReadPassword reads a value from the terminal with no echo. The prompt is 71 | // ephemeral. 72 | func ReadPassword(prompt string) (s []byte, err error) { 73 | err = withTerminal(func(in, out *os.File) error { 74 | fmt.Fprintf(out, "%s ", prompt) 75 | defer clearLine(out) 76 | s, err = term.ReadPassword(int(in.Fd())) 77 | return err 78 | }) 79 | return 80 | } 81 | 82 | // WriteFile writes data to the named file. If the file exists it will ask 83 | // for confirmation before overwriting it. 84 | func WriteFile(name string, data []byte, perm os.FileMode) error { 85 | st, err := os.Stat(name) 86 | if err != nil { 87 | if os.IsNotExist(err) { 88 | return os.WriteFile(name, data, perm) 89 | } 90 | return err 91 | } 92 | 93 | if st.IsDir() { 94 | return fmt.Errorf("file %q is a directory", name) 95 | } 96 | 97 | c, err := readCharacter("Would you like to overwrite %q [y/n]: ", name) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | for { 103 | switch c { 104 | case 'y', 'Y': 105 | return os.WriteFile(name, data, perm) 106 | case 'n', 'N': 107 | return os.ErrExist 108 | case '\x03': // CTRL-C 109 | return fmt.Errorf("user canceled prompt") 110 | default: 111 | c, err = readCharacter("Invalid selection %q. Would you like to overwrite %q [y/n]: ", c, name) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | } 117 | } 118 | 119 | // readCharacter reads a single character from the terminal with no echo. The 120 | // prompt is ephemeral. 121 | func readCharacter(prompt string, args ...any) (c byte, err error) { 122 | err = withTerminal(func(in, out *os.File) error { 123 | fmt.Fprintf(out, prompt, args...) 124 | defer clearLine(out) 125 | 126 | oldState, err := term.MakeRaw(int(in.Fd())) 127 | if err != nil { 128 | return err 129 | } 130 | defer term.Restore(int(in.Fd()), oldState) 131 | 132 | b := make([]byte, 1) 133 | if _, err := in.Read(b); err != nil { 134 | return err 135 | } 136 | 137 | c = b[0] 138 | return nil 139 | }) 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Smallstep Labs, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package main 15 | 16 | import ( 17 | "github.com/smallstep/step-kms-plugin/cmd" 18 | 19 | // KMS modules 20 | _ "go.step.sm/crypto/kms/awskms" 21 | _ "go.step.sm/crypto/kms/azurekms" 22 | _ "go.step.sm/crypto/kms/capi" 23 | _ "go.step.sm/crypto/kms/cloudkms" 24 | _ "go.step.sm/crypto/kms/mackms" 25 | _ "go.step.sm/crypto/kms/pkcs11" 26 | _ "go.step.sm/crypto/kms/softkms" 27 | _ "go.step.sm/crypto/kms/sshagentkms" 28 | _ "go.step.sm/crypto/kms/tpmkms" 29 | _ "go.step.sm/crypto/kms/yubikey" 30 | ) 31 | 32 | func main() { 33 | cmd.Execute() 34 | } 35 | -------------------------------------------------------------------------------- /scripts/package-repo-import.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | : ${GCLOUD_LOCATION:=us-central1} 6 | : ${GCLOUD_RPM_REPO:=rpms} 7 | : ${GCLOUD_DEB_REPO:=debs} 8 | 9 | PACKAGE="${1}" 10 | VERSION="${2}" 11 | RELEASE="1" 12 | EPOCH="0" 13 | GORELEASER_PHASE=${GORELEASER_PHASE:-release} 14 | 15 | echo "Package: ${PACKAGE}" 16 | echo "Version: ${VERSION}" 17 | 18 | check_package() { 19 | local EXITCODE=0 20 | local REPO="${1}" 21 | local VER="${2}" 22 | if [ ! -f /tmp/version-deleted.stamp ]; then 23 | gcloud artifacts versions list \ 24 | --repository "${REPO}" \ 25 | --location "${GCLOUD_LOCATION}" \ 26 | --package "${PACKAGE}" \ 27 | --filter "VERSION:${VER}" \ 28 | --format json 2> /dev/null \ 29 | | jq -re '.[].name?' >/dev/null 2>&1 \ 30 | || EXITCODE=$? 31 | if [[ "${EXITCODE}" -eq 0 ]]; then 32 | echo "Package version already exists. Removing it..." 33 | gcloud artifacts versions delete \ 34 | --quiet "${VER}" \ 35 | --package "${PACKAGE}" \ 36 | --repository "${REPO}" \ 37 | --location "${GCLOUD_LOCATION}" 38 | touch /tmp/version-deleted.stamp 39 | fi 40 | fi 41 | } 42 | 43 | if [[ ${IS_PRERELEASE} == "true" ]]; then 44 | echo "Skipping artifact import; IS_PRERELEASE is 'true'" 45 | exit 0; 46 | fi 47 | 48 | check_package "${GCLOUD_RPM_REPO}" "${EPOCH}:${VERSION}-${RELEASE}" 49 | gcloud artifacts yum import "${GCLOUD_RPM_REPO}" \ 50 | --location "${GCLOUD_LOCATION}" \ 51 | --gcs-source "gs://artifacts-outgoing/${PACKAGE}/rpm/${VERSION}/*" 52 | 53 | check_package ${GCLOUD_DEB_REPO} "${VERSION}-${RELEASE}"} 54 | gcloud artifacts apt import "${GCLOUD_DEB_REPO}" \ 55 | --location "${GCLOUD_LOCATION}" \ 56 | --gcs-source "gs://artifacts-outgoing/${PACKAGE}/deb/${VERSION}/*" 57 | -------------------------------------------------------------------------------- /scripts/package-upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | FILE="${1}" 7 | PACKAGE="${2}" 8 | VERSION="${3}" 9 | 10 | 11 | echo "Package File: ${FILE}" 12 | echo "Package: ${PACKAGE}" 13 | echo "Version: ${VERSION}" 14 | echo "Prerelease: ${IS_PRERELEASE}" 15 | 16 | if [[ ${IS_PRERELEASE} == "true" ]]; then 17 | echo "Skipping artifact upload; IS_PRERELEASE is 'true'" 18 | exit 0; 19 | fi 20 | 21 | if [ "${FILE: -4}" == ".deb" ]; then 22 | if [[ "${FILE}" =~ "armhf6" ]]; then 23 | echo "Skipping ${FILE} due to GCP Artifact Registry armhf conflict!" 24 | else 25 | gcloud storage cp ${FILE} gs://artifacts-outgoing/${PACKAGE}/deb/${VERSION}/ 26 | fi 27 | else 28 | gcloud storage cp ${FILE} gs://artifacts-outgoing/${PACKAGE}/rpm/${VERSION}/ 29 | fi 30 | --------------------------------------------------------------------------------