diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e0871f93..a9618d0b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,7 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/actionci.yml b/.github/workflows/actionci.yml new file mode 100644 index 00000000..f8482e66 --- /dev/null +++ b/.github/workflows/actionci.yml @@ -0,0 +1,22 @@ +name: Action CI + +on: + push: + tags-ignore: + - 'v*' + branches: + - "master" + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + actionci: + permissions: + contents: read + actions: read + security-events: write + uses: smallstep/workflows/.github/workflows/actionci.yml@main + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f37823a..7ea60dac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,7 @@ jobs: security-events: write uses: smallstep/workflows/.github/workflows/goCI.yml@main with: + only-latest-golang: false run-codeql: true + golangci-lint-version: "v2.12.1" secrets: inherit diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..b145ea96 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,10 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: + pull-requests: read + +jobs: + dependabot-auto-merge: + uses: smallstep/workflows/.github/workflows/dependabot-auto-merge.yml@main + secrets: inherit diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 00000000..8474c355 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,77 @@ +name: Publish to packages.smallstep.com + +# Independently publish packages to Red Hat (RPM) and Debian (DEB) repositories +# without running a full release. Downloads packages from GitHub releases, +# uploads to GCS, and imports to Artifact Registry. +# +# Usage (CLI): +# gh workflow run publish-packages.yml -f tag=v0.28.0 + +on: + workflow_dispatch: + inputs: + tag: + description: 'Git tag to publish (e.g., v0.28.0)' + required: true + type: string + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + + - name: Extract version + id: version + run: echo "version=${TAG#v}">> "$GITHUB_OUTPUT" + env: + TAG: ${{ inputs.tag }} + + - name: Is Pre-release + id: is_prerelease + run: | + if [[ "$TAG" == *"-rc"* ]]; then + echo "is_prerelease=true">> "$GITHUB_OUTPUT" + else + echo "is_prerelease=false">> "$GITHUB_OUTPUT" + fi + env: + TAG: ${{ inputs.tag }} + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_CLOUD_GITHUB_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + with: + project_id: ${{ secrets.GOOGLE_CLOUD_PACKAGES_PROJECT_ID }} + + - name: Download packages from GitHub release + run: | + mkdir -p dist + gh release download "$TAG" --pattern "*${VERSION}*.deb" --pattern "*${VERSION}*.rpm" --dir dist + env: + TAG: ${{ inputs.tag }} + VERSION: ${{ steps.version.outputs.version }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload packages to GCS + run: | + for pkg in dist/*.deb dist/*.rpm; do + ./scripts/package-upload.sh "$pkg" step-cli ${{ steps.version.outputs.version }} + done + + - name: Import packages to Artifact Registry + run: ./scripts/package-repo-import.sh step-cli ${{ steps.version.outputs.version }} + env: + IS_PRERELEASE: ${{ steps.is_prerelease.outputs.is_prerelease }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebdf6a54..5781e95a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,175 +6,127 @@ on: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 +permissions: + contents: read + jobs: ci: permissions: actions: read contents: read security-events: write - uses: smallstep/cli/.github/workflows/ci.yml@master + uses: ./.github/workflows/ci.yml secrets: inherit - create_release: - name: Create Release + release_metadata: + name: Release Metadata + permissions: + contents: read needs: ci runs-on: ubuntu-latest env: DOCKER_IMAGE: smallstep/step-cli + DEBIAN_TAG: trixie outputs: version: ${{ steps.extract-tag.outputs.VERSION }} vversion: ${{ steps.extract-tag.outputs.VVERSION }} is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} docker_tags: ${{ env.DOCKER_TAGS }} + docker_tags_debian: ${{ env.DOCKER_TAGS_DEBIAN }} steps: - name: Is Pre-release id: is_prerelease + env: + REF: ${{ github.ref }} run: | set +e - echo ${{ github.ref }} | grep "\-rc.*" + echo "${REF}" | grep "\-rc.*" OUT=$? if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi - echo "IS_PRERELEASE=${IS_PRERELEASE}">> ${GITHUB_OUTPUT} + echo "IS_PRERELEASE=${IS_PRERELEASE}">> "${GITHUB_OUTPUT}" - name: Extract Tag Names id: extract-tag run: | VVERSION=${GITHUB_REF#refs/tags/} VERSION=${GITHUB_REF#refs/tags/v} - echo "VVERSION=${VVERSION}">> ${GITHUB_OUTPUT} - echo "VERSION=${VERSION}">> ${GITHUB_OUTPUT} - echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}">> ${GITHUB_ENV} + echo "VVERSION=${VVERSION}">> "${GITHUB_OUTPUT}" + echo "VERSION=${VERSION}">> "${GITHUB_OUTPUT}" + echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}">> "${GITHUB_ENV}" + echo "DOCKER_TAGS_DEBIAN=${{ env.DOCKER_IMAGE }}:${VERSION}-${DEBIAN_TAG}">> "${GITHUB_ENV}" - name: Add Latest Tag if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false' run: | - echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest">> ${GITHUB_ENV} - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} + echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest">> "${GITHUB_ENV}" + echo "DOCKER_TAGS_DEBIAN=${{ env.DOCKER_TAGS_DEBIAN }},${{ env.DOCKER_IMAGE }}:${DEBIAN_TAG}">> "${GITHUB_ENV}" goreleaser: - name: Upload Assets to GitHub w/ goreleaser - runs-on: ubuntu-latest - needs: create_release + needs: release_metadata permissions: id-token: write contents: write - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.19 - check-latest: true - - name: Configure Go - id: configure_go - run: | - PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin - - name: Install cosign - uses: sigstore/cosign-installer@v2 - with: - cosign-release: 'v1.13.1' - - name: Get Release Date - id: release_date - run: | - RELEASE_DATE=$(date +"%y-%m-%d") - echo "RELEASE_DATE=${RELEASE_DATE}">> ${GITHUB_OUTPUT} - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 - with: - version: 'latest' - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} - RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }} - COSIGN_EXPERIMENTAL: 1 + packages: write + uses: smallstep/workflows/.github/workflows/goreleaser.yml@main + with: + enable-packages-upload: true + is-prerelease: ${{ needs.release_metadata.outputs.is_prerelease == 'true' }} + secrets: inherit build_upload_docker: name: Build & Upload Docker Images - needs: create_release + needs: release_metadata permissions: id-token: write - contents: write + contents: read uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main with: platforms: linux/amd64,linux/386,linux/arm,linux/arm64 - tags: ${{ needs.create_release.outputs.docker_tags }} + tags: ${{ needs.release_metadata.outputs.docker_tags }} docker_image: smallstep/step-cli docker_file: docker/Dockerfile secrets: inherit -# All jobs below this are for full releases (non release candidates e.g. *-rc.*) + build_upload_docker_debian: + name: Build & Upload Docker Images using Debian + needs: release_metadata + permissions: + id-token: write + contents: read + uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main + with: + platforms: linux/amd64,linux/386,linux/arm,linux/arm64 + tags: ${{ needs.release_metadata.outputs.docker_tags_debian }} + docker_image: smallstep/step-cli + docker_file: docker/Dockerfile.debian + secrets: inherit - build_upload_aws_s3_binaries: - name: Build & Upload AWS S3 Binaries - runs-on: ubuntu-latest - needs: create_release - if: needs.create_release.outputs.is_prerelease == 'false' - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: 1.19 - check-latest: true - - name: Build - id: build - run: | - PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin - make -j1 binary-linux-amd64 binary-linux-arm64 binary-darwin-amd64 binary-windows-amd64 - mkdir -p ./.releases - cp ./output/binary/linux-amd64/bin/step ./.releases/step_${{ needs.create_release.outputs.version }}_linux_amd64 - cp ./output/binary/linux-amd64/bin/step ./.releases/step_latest_linux_amd64 - cp ./output/binary/linux-arm64/bin/step ./.releases/step_${{ needs.create_release.outputs.version }}_linux_arm64 - cp ./output/binary/linux-arm64/bin/step ./.releases/step_latest_linux_arm64 - cp ./output/binary/darwin-amd64/bin/step ./.releases/step_${{ needs.create_release.outputs.version }}_darwin_amd64 - cp ./output/binary/darwin-amd64/bin/step ./.releases/step_latest_darwin_amd64 - cp ./output/binary/windows-amd64/bin/step ./.releases/step_${{ needs.create_release.outputs.version }}_windows.exe - cp ./output/binary/windows-amd64/bin/step ./.releases/step_latest_windows.exe - - name: Upload s3 - id: upload-s3 - uses: jakejarvis/s3-sync-action@v0.5.1 - with: - args: --acl public-read --follow-symlinks - env: - AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-east-1 - SOURCE_DIR: ./.releases +# All jobs below this are for full releases (non release candidates e.g. *-rc.*) update_reference_docs: name: Update Reference Docs + permissions: + contents: read runs-on: ubuntu-latest - needs: create_release - if: needs.create_release.outputs.is_prerelease == 'false' + needs: release_metadata + if: needs.release_metadata.outputs.is_prerelease == 'false' steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: 1.19 + go-version: 'stable' check-latest: true - name: Build id: build run: V=1 make build - name: Checkout Docs - uses: actions/checkout@master + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: smallstep/docs token: ${{ secrets.DOCS_PAT }} path: './docs' - name: Setup bot SSH signing key - uses: webfactory/ssh-agent@v0.7.0 + uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 env: HAS_SSH_PRIVATE_KEY: ${{ secrets.STEP_TRAVIS_CI_GH_PRIVATE_SIGNING_KEY != '' }} if: ${{ env.HAS_SSH_PRIVATE_KEY == 'true' }} @@ -184,19 +136,42 @@ jobs: - name: Update Reference id: update_reference run: | - ./bin/step help --markdown ./docs/src/pages/docs/step-cli/reference cd ./docs + git config user.email "eng+ci@smallstep.com" - git config user.name "step-travis-ci" + git config user.name "step-ci" # Configure GH commit signing key. git config --global commit.gpgsign true git config --global gpg.format ssh git config --global user.signingkey "${{ secrets.STEP_TRAVIS_CI_GH_PUBLIC_SIGNING_KEY }}" - git add . && git commit -a -m "step-cli ${{ needs.create_release.outputs.vversion }} reference update" + # Remove old docs + git rm -rf ./step-cli/reference + + # Build fresh docs + ../bin/step help --markdown ./step-cli/reference + + # Generate new route manifest for the docs + find step-cli/reference -mindepth 2 -type f | jq -R -s '[ + split("\n")[:-1][] + | {hideFromSidebar: true, + title: "", + path: ("/" + .)} + ]'> "$RUNNER_TEMP/reference-routes.json" + + # Replace old route manifest with new + jq --slurpfile newRoutes "$RUNNER_TEMP/reference-routes.json" 'walk( + if type == "object" and .isStepReference == true then + .routes = $newRoutes[0] + else . end + )' < manifest.json> manifest.json.new + + mv manifest.json.new manifest.json + + git add . && git commit -a -m "step-cli ${{ needs.release_metadata.outputs.vversion }} reference update" - name: Push changes - uses: ad-m/github-push-action@v0.6.0 + uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # v1.3.0 with: github_token: ${{ secrets.DOCS_PAT }} branch: 'main' diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index f1363a4b..9f73ee33 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -10,6 +10,10 @@ on: - opened - reopened +permissions: + issues: write + pull-requests: write + jobs: triage: uses: smallstep/workflows/.github/workflows/triage.yml@main diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..92a61467 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,12 @@ +rules: + unpinned-uses: + config: + policies: + "smallstep/*": ref-pin + secrets-inherit: + disable: true + ref-confusion: + disable: true + dangerous-triggers: + ignore: + - triage.yml diff --git a/.gitignore b/.gitignore index c399faf0..1ae321b4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,11 @@ go.work.sum coverage.txt output vendor +dist/ step .idea .envrc + +# Packages files +0x889B19391F774443-Certify.key +gha-creds-*.json diff --git a/.gitleaksignore b/.gitleaksignore deleted file mode 100644 index d0457e3b..00000000 --- a/.gitleaksignore +++ /dev/null @@ -1,9 +0,0 @@ -a62a7fa71c4c76aef9632e16e0d7322361ff6d2a:command/oauth/cmd.go:generic-api-key:49 -a62a7fa71c4c76aef9632e16e0d7322361ff6d2a:command/oauth/cmd.go:generic-api-key:53 -85fa03947fa46a0c660a795ba41c4ebfd0179dc9:command/oauth/cmd.go:generic-api-key:48 -85fa03947fa46a0c660a795ba41c4ebfd0179dc9:command/oauth/cmd.go:generic-api-key:51 -bc414076c690a306691655936431125fd1b6ddf4:command/oauth/cmd.go:generic-api-key:51 -19830a88a42f6e166ec34d3af991db130f0aa5a6:crypto/certificates/go-x509/x509_test_import.go:private-key:44 -043f6b09fbf370f862de63b5f4d065f2475ac35d:command/crypto/jwe/jwe.go:generic-api-key:130 -07d2176bf034c900b5aa273ef97e34bf8ce9cec5:command/oauth/cmd.go:generic-api-key:43 -07d2176bf034c900b5aa273ef97e34bf8ce9cec5:crypto/keys/key.go:generic-api-key:29 diff --git a/.goreleaser.yml b/.goreleaser.yml index 519a81d4..d5950cdf 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,18 +1,38 @@ -# This is an example .goreleaser.yml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com +# Documentation: https://goreleaser.com/customization/ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json +version: 2 project_name: step +# Enable GoReleaser OSS to read Pro configs: https://goreleaser.com/errors/version/#using-a-pro-configuration-file-with-goreleaser-oss +pro: true + +variables: + packageName: step-cli + packageRelease: 1 # Manually update release: in the nfpm section to match this value if you change this + before: hooks: - # You may remove this if you don't use go modules. - go mod download - # - go generate ./... + +after: + hooks: + # This script depends on IS_PRERELEASE env being set. This is set by CI in the Is Pre-release step. + - cmd: bash scripts/package-repo-import.sh {{ .Var.packageName }} {{ .Version }} + output: true builds: - - + - &BUILD id: default env: - CGO_ENABLED=0 + main: ./cmd/step + flags: + - -trimpath + ldflags: + - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} + gcflags: + ->- + {{- if ne (index .Env "DEBUG") "" }}all=-N -l{{- end }} targets: - darwin_amd64 - darwin_arm64 @@ -28,47 +48,38 @@ builds: - linux_ppc64le - windows_amd64 - windows_arm64 - flags: - - -trimpath - main: ./cmd/step/main.go binary: bin/step - ldflags: - - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - # This build is specifically for nFPM targets (.deb and .rpm files). - # It's exactly the same as the default build above, except: - # - it only builds the archs we want to produce .deb and .rpm files for - # - the name of the output binary is step-cli + # It's exactly the same as the default build above, except the binary is + # named step-cli. It inherits all Linux targets from the default build. + << : *BUILD id: nfpm - env: - - CGO_ENABLED=0 - goos: - - linux - goarch: - - amd64 - flags: - - -trimpath - main: ./cmd/step/main.go binary: step-cli - ldflags: - - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} archives: - - + - &ARCHIVE + id: default # Can be used to change the archive formats for specific GOOSs. # Most common use case is to archive as zip on Windows. # Default is empty. name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" format_overrides: - goos: windows - format: zip - builds: + formats: ['zip'] + ids: - default wrap_in_directory: "{{ .ProjectName }}_{{ .Version }}" files: - README.md - LICENSE - autocomplete/* + - + << : *ARCHIVE + id: unversioned + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + wrap_in_directory: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + nfpms: # Configure nFPM for .deb and .rpm releases @@ -80,11 +91,17 @@ nfpms: # List file contents: dpkg -c dist/step_...deb # Package metadata: dpkg --info dist/step_....deb # - - - builds: + - &NFPM + id: packages + ids: - nfpm - package_name: step-cli - file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + package_name: "{{ .Var.packageName }}" + release: "1" + file_name_template:>- + {{- trimsuffix .ConventionalFileName .ConventionalExtension -}} + {{- if and (eq .Arm "6") (eq .ConventionalExtension ".deb") }}6{{ end -}} + {{- if not (eq .Amd64 "v1")}}{{ .Amd64 }}{{ end -}} + {{- .ConventionalExtension -}} vendor: Smallstep Labs homepage: https://github.com/smallstep/cli maintainer: Smallstep @@ -98,7 +115,6 @@ nfpms: - deb - rpm priority: optional - bindir: /usr/bin contents: - src: debian/copyright @@ -111,6 +127,17 @@ nfpms: scripts: postinstall: scripts/postinstall.sh postremove: scripts/postremove.sh + rpm: + signature: + key_file: '{{ envOrDefault "GPG_PRIVATE_KEY_FILE" "ENV_VAR_GPG_PRIVATE_KEY_FILE_NOT_SET" }}' + deb: + signature: + key_file: '{{ envOrDefault "GPG_PRIVATE_KEY_FILE" "ENV_VAR_GPG_PRIVATE_KEY_FILE_NOT_SET" }}' + type: origin + - + << : *NFPM + id: unversioned + file_name_template: "{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" source: enabled: true @@ -123,13 +150,22 @@ checksum: signs: - cmd: cosign - signature: "${artifact}.sig" - certificate: "${artifact}.pem" - args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"] + signature: "${artifact}.sigstore.json" + args: + - "sign-blob" + - "--bundle=${signature}" + - "${artifact}" + - "--yes" artifacts: all +publishers: +- name: Google Cloud Artifact Registry + ids: + - packages + cmd: ./scripts/package-upload.sh {{ abs .ArtifactPath }} {{ .Var.packageName }} {{ .Version }} {{ .Var.packageRelease }} + snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" release: # Repo in which the release will be created. @@ -164,27 +200,31 @@ release: header: | ## Official Release Artifacts - #### Linux + Below are the most popular artifacts for `step` on each platform. - - 📦 [step_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step_linux_{{ .Version }}_amd64.tar.gz) - - 📦 [step-cli_{{ .Version }}_amd64.deb](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli_{{ .Version }}_amd64.deb) - - 📦 [step-cli_{{ .Version }}_amd64.rpm](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli_{{ .Version }}_amd64.rpm) + For packaged versions (Homebrew, Scoop, etc.), see our [installation docs](https://smallstep.com/docs/step-cli/installation). + + #### Linux + - 📦 [step_linux_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_linux_{{ .Version }}_amd64.tar.gz) + - 📦 [step_linux_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_linux_{{ .Version }}_arm64.tar.gz) + - 📦 [step_linux_{{ .Version }}_armv7.tar.gz](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_linux_{{ .Version }}_armv7.tar.gz) + - 📦 [step-cli_{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}_amd64.deb](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli_{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}_amd64.deb) + - 📦 [step-cli-{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}.x86_64.rpm](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli-{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}.x86_64.rpm) + - 📦 [step-cli_{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}_arm64.deb](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli_{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}_arm64.deb) + - 📦 [step-cli-{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}.aarch64.rpm](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step-cli-{{ replace .Version "-" "." }}-{{ .Var.packageRelease }}.aarch64.rpm) + - see `Assets` below for more builds #### macOS Darwin - - 📦 [step_darwin_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step_darwin_{{ .Version }}_amd64.tar.gz) - - 📦 [step_darwin_{{ .Version }}_arm64.tar.gz](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step_darwin_{{ .Version }}_arm64.tar.gz) + - 📦 [step_darwin_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_darwin_{{ .Version }}_amd64.tar.gz) + - 📦 [step_darwin_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_darwin_{{ .Version }}_arm64.tar.gz) #### Windows - - 📦 [step_windows_{{ .Version }}_amd64.zip](https://dl.step.sm/gh-release/cli/gh-release-header/{{ .Tag }}/step_windows_{{ .Version }}_amd64.zip) - - For more builds across platforms and architectures see the `Assets` section below. - And for packaged versions (Homebrew, Scoop, etc.), see our [installation docs](https://smallstep.com/docs/step-cli/installation). + - 📦 [step_windows_{{ .Version }}_amd64.zip](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_windows_{{ .Version }}_amd64.zip) + - 📦 [step_windows_{{ .Version }}_arm64.zip](https://dl.smallstep.com/gh-release/cli/gh-release-header/{{ .Tag }}/step_windows_{{ .Version }}_arm64.zip) - Don't see the artifact you need? Open an issue [here](https://github.com/smallstep/cli/issues/new/choose). - ## Signatures and Checksums `step` uses [sigstore/cosign](https://github.com/sigstore/cosign) for signing and verifying release artifacts. @@ -192,9 +232,10 @@ release: Below is an example using `cosign` to verify a release artifact: ``` - COSIGN_EXPERIMENTAL=1 cosign verify-blob \ - --certificate ~/Download/step_darwin_{{ .Version }}_amd64.tar.gz.pem \ - --signature ~/Downloads/step_darwin_{{ .Version }}_amd64.tar.gz.sig \ + cosign verify-blob \ + --bundle ~/Downloads/step_darwin_{{ .Version }}_amd64.tar.gz.sigstore.json \ + --certificate-identity-regexp "https://github\.com/smallstep/workflows/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ~/Downloads/step_darwin_{{ .Version }}_amd64.tar.gz ``` @@ -207,7 +248,7 @@ release: Those were the changes on {{ .Tag }}! - Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peak at the freshest PKI memes. + Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peek at the freshest PKI memes. # You can disable this pipe in order to not upload any artifacts. # Defaults to false. @@ -222,41 +263,226 @@ release: # - glob: ./glob/**/to/**/file/**/* # - glob: ./glob/foo/to/bar/file/foobar/override_from_previous -scoop: - # Template for the url which is determined by the given Token (github or gitlab) - # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" - # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" - # Default for gitea is "https://gitea.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" - url_template: "http://github.com/smallstep/cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - - # Repository to push the app manifest to. - bucket: - owner: smallstep - name: scoop-bucket - - # Git author used to commit to the repository. - # Defaults are shown. - commit_author: - name: goreleaserbot - email: goreleaser@smallstep.com - - # The project name and current git tag are used in the format string. - commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" - - # Your app's homepage. - # Default is empty. - homepage: "https://smallstep.com/" +blobs: + - &S3_VERSIONED + provider: s3 + disable: 'false' + ids: + - default + bucket: '{{ .Env.AWS_S3_BUCKET }}' + region: '{{ .Env.AWS_S3_REGION }}' + directory: '/' + acl: public-read + extra_files: + - glob: ./dist/default_darwin_amd64_v1/bin/step + name_template: step_{{ .Version }}_darwin_amd64 + - glob: ./dist/default_darwin_arm64*/bin/step + name_template: step_{{ .Version }}_darwin_arm64 + - glob: ./dist/default_linux_amd64_v1/bin/step + name_template: step_{{ .Version }}_linux_amd64 + - glob: ./dist/default_linux_arm64*/bin/step + name_template: step_{{ .Version }}_linux_arm64 + - glob: ./dist/default_windows_amd64_v1/bin/step.exe + name_template: step_{{ .Version }}_windows_amd64.exe + - glob: ./dist/default_freebsd_*/bin/step + name_template: step_{{ .Version }}_freebsd_amd64 + extra_files_only: true + + # Unversioned (`latest`) copies of binaries. + # This section should only run on full releases (not prereleases). + - + << : *S3_VERSIONED + disable: '{{ if .Prerelease }}true{{ else }}false{{ end }}' + extra_files: + - glob: ./dist/default_darwin_amd64_v1/bin/step + name_template: step_latest_darwin_amd64 + - glob: ./dist/default_darwin_arm64*/bin/step + name_template: step_latest_darwin_arm64 + - glob: ./dist/default_linux_amd64_v1/bin/step + name_template: step_latest_linux_amd64 + - glob: ./dist/default_linux_arm64*/bin/step + name_template: step_latest_linux_arm64 + - glob: ./dist/default_windows_amd64_v1/bin/step.exe + name_template: step_latest_windows_amd64.exe + - glob: ./dist/default_freebsd_*/bin/step + name_template: step_latest_freebsd_amd64 + extra_files_only: true + +winget: + - + # IDs of the archives to use. + # Empty means all IDs. + ids: [ default ] + + # + # Default: ProjectName + # Templates: allowed + name: step + + # Publisher name. + # + # Templates: allowed + # Required. + publisher: Smallstep + + # Your app's description. + # + # Templates: allowed + # Required. + short_description: "A Swiss army knife for working with X.509 certificates, JWTs, etc." + + # License name. + # + # Templates: allowed + # Required. + license: "Apache-2.0" + + # Publisher URL. + # + # Templates: allowed + publisher_url: "https://smallstep.com" + + # Publisher support URL. + # + # Templates: allowed + publisher_support_url: "https://github.com/smallstep/certificates/discussions" + + # URL which is determined by the given Token (github, gitlab or gitea). + # + # Default depends on the client. + # Templates: allowed + url_template: "https://github.com/smallstep/cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + + # Git author used to commit to the repository. + commit_author: + name: goreleaserbot + email: goreleaser@smallstep.com + + # The project name and current git tag are used in the format string. + # + # Templates: allowed + commit_msg_template: "{{ .PackageIdentifier }}: {{ .Tag }}" + + # Your app's homepage. + homepage: "https://github.com/smallstep/cli" + + # Your app's long description. + # + # Templates: allowed + description: | + step-cli lets you build, operate, and automate Public Key Infrastructure (PKI) + systems and workflows. It's a swiss army knife for authenticated encryption + (X.509, TLS), single sign-on (OAuth OIDC, SAML), multi-factor authentication + (OATH OTP, FIDO U2F), encryption mechanisms (JSON Web Encryption, NaCl), + and verifiable claims (JWT, SAML assertions). + + # License URL. + # + # Templates: allowed + license_url: "https://github.com/smallstep/cli/blob/master/LICENSE" + + # Release notes URL. + # + # Templates: allowed + release_notes_url: "https://github.com/smallstep/cli/releases/tag/{{ .Tag }}" + + # Create the PR - for testing + skip_upload: auto + + # Privacy URL. + # + # Templates: allowed + privacy_url: "https://smallstep.com/privacy-policy" + + # Tags. + tags: + - cli + - smallstep + - pki + - x509 + - certificates + - tls + - ssl + - jwt + - oauth + - security + - encryption + - cryptography + + # Repository to push the generated files to. + repository: + owner: smallstep + name: winget-pkgs + branch: "step-{{.Version}}" + + # Optionally a token can be provided, if it differs from the token + # provided to GoReleaser + # Templates: allowed + #token: "{{ .Env.GITHUB_PERSONAL_AUTH_TOKEN }}" + + # Sets up pull request creation instead of just pushing to the given branch. + # Make sure the 'branch' property is different from base before enabling + # it. + # + # Since: v1.17 + pull_request: + # Whether to enable it or not. + enabled: true + check_boxes: true + # Whether to open the PR as a draft or not. + # + # Default: false + # Since: v1.19 + # draft: true + + # Base can also be another repository, in which case the owner and name + # above will be used as HEAD, allowing cross-repository pull requests. + # + # Since: v1.19 + base: + owner: microsoft + name: winget-pkgs + branch: master + +scoops: + - + ids: [ default ] + name: step + # Template for the url which is determined by the given Token (github or gitlab) + # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" + # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" + # Default for gitea is "https://gitea.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" + url_template: "https://github.com/smallstep/cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + + # Repository to push the app manifest to. + repository: + owner: smallstep + name: scoop-bucket + branch: main + + # Git author used to commit to the repository. + # Defaults are shown. + commit_author: + name: goreleaserbot + email: goreleaser@smallstep.com + + # The project name and current git tag are used in the format string. + commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" + + # Your app's homepage. + # Default is empty. + homepage: "https://smallstep.com/" - # Skip uploads for prerelease. - skip_upload: auto + # Skip uploads for prerelease. + skip_upload: auto - # Your app's description. - # Default is empty. - description: "Crypto toolkit for working with X.509, OAuth, JWT, OATH OTP, etc." + # Your app's description. + # Default is empty. + description: "Crypto toolkit for working with X.509, OAuth, JWT, OATH OTP, etc." - # Your app's license - # Default is empty. - license: "Apache-2.0" + # Your app's license + # Default is empty. + license: "Apache-2.0" #dockers: # - dockerfile: docker/Dockerfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb6c564..0b208082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Support for inspecting certificates with post-quantum algorithms ML-DSA and + SLH-DSA (smallstep/certinfo#69). + ### Changed ### Deprecated @@ -26,11 +29,458 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. --- -## [Unreleased] +## [0.30.3] - 2026年06月09日 + +### Added + +- Add support for inspecting certificates with post-quantum algorithms ML-DSA and + SLH-DSA (smallstep/certinfo#69, smallstep/cli#1605). + +### Fixed + +- Fix file paths being misidentified as KMS URIs when the path didn't match a KMS + URI pattern; detection now uses an existence check instead (smallstep/cli#1604). +- Fix `step ssh proxycommand` hanging when the server closes the connection before + stdin is closed (smallstep/cli#1647). + + +## [0.30.2] - 2026年03月22日 + +- Update certificates to v0.30.2 + + +## [0.30.1] - 2026年03月18日 + +- Fix release issue + + +## [0.30.0] - 2026年03月18日 + +### Added + +- Allow using KMS URIs directly without the `--kms` flag for commands that use + the cryptoutils package (smallstep/cli#1560). + +### Changed + +- Expand `--kms` flag help text with detailed documentation for all supported + KMS types (YubiKey PIV, PKCS #11, TPM 2.0, Google Cloud KMS, AWS KMS, Azure + Key Vault) and usage examples (smallstep/cli#1550). +- Prefer `verification_uri_complete` over `verification_uri` in the OIDC + Device Authorization Flow when the IdP provides it, so users don't need to + manually enter a code (smallstep/cli#1430). +- Skip printing the user code during OIDC device authorization when the + complete verification URI already embeds it (smallstep/cli#1595). +- Suppress output messages for `step certificate needs-renewal` and `step ssh + needs-renewal` commands when certificates don't need renewal. Use the + `--verbose` flag to always show messages regardless of renewal status + (smallstep/cli#1548). + +### Fixed + +- Overwrite file when using --force with step crypto key format (smallstep/cli#1581) + + +## [0.29.0] - 2025年12月02日 + +### Added + +- Add PKIX fingerprint support for `step crypto key fingerprint` (smallstep/cli#1474) +- Add remote configuration of the provisioner GCP organization id (smallstep/cli#1490) + +### Changed + +- Do not create an identity token if it's not enabled (smallstep/cli#1495). +- Make --attestation-uri incompatible with --kms for `step ca certificate` (smallstep/cli#1516) + +## [0.28.7] - 2025年07月13日 + +### Added + +- Add support for specifying key usage, extended key usage, and basic constraints + in certificate requests (smallstep/crypto#767) +- Ensure HOMEDRIVE is used, on Windows, when locating SSH config file (smallstep/cli#1434) + +### Changed + +- Enable alternate SSH agents for `step ssh` on Windows (smallstep/cli#1428) +- Refactor CLI to enable testing via testscript (smallstep/cli#1426) + +### Fixed + +- Fix step ca token help text around validity period flags (smallstep/cli#1411) +- Fix some provisioner and policy prompt issues (smallstep/cli#1391) + * SCEP provisioners not detected in admin token flows. They now return an error, + similar to ACME provisioners, if selected. + * Invalid provisioner selection logic when managing provisioner policies. + The --provisioner flag was used to select a provisioner to authenticate + as well as the provisioner to manage policies for. + * Unexpected error messages showing "issuer" instead of "provisioner" flag. In certain + situations the CLI would return error messages indicating an issue with the --issuer + flag value, whereas it was actually supplied in the --provisioner flag. + + +## [0.28.6] - 2025年03月17日 + +- dependabot updates + +## [0.28.5] - 2025年03月05日 + +- v0.28.4 skipped due to broken CI + +### Added + +- Add the --set and --set-file flags to the step ca token command, allowing the user to set keys in the "user" claim in the resulting JWT. (smallstep/cli#1375) +- Support for downloading additional default settings when running 'step ssh config' (smallstep/cli#1377) + - 'min-password-length' and 'provisioner' + + +## [0.28.3] - 2025年02月20日 + +### Added + +- Add support for KMS in the ca renew and rekey commands (smallstep/cli#1353) + +### Fixed + +- Correctly handle redirect-url flag when bootstrapping (smallstep/cli#1350) + + +## [0.28.2] - 2024年11月20日 + +### Fixed + +- Broken release process + + +## [0.28.1] - 2024年11月19日 + +### Changed + +- Updated smallstep/certinfo package (smallstep/cli#1309) + + +## [0.28.0] - 2024年10月29日 + +### Added + +- disableSSHCAUser and disableSSHCAHost options to GCP provisioner create and update commands (smallstep/cli#1305) +- Support programmatically opening browser on Android devices (smallstep/cli#1301) + +### Fixed + +- Fix --context being ignored in commands that rely on certificates (smallstep/cli#1301) + + +## [0.27.5] - 2024年10月17日 + +### Added + +- Add `--remove-scope` flag to provisioner update command. Removes the given + scope, used to validate the scopes extension in an OpenID Connect token (smallstep/cli#1287) + + +## [0.27.4] - 2024年09月13日 + +### Added + +- Support for signing and publishing RPM and Deb packages to GCP Artifact Registry (smallstep/cli#1246) + +### Changed + +- Update Release download URLs for RPM and DEB packages with new file name formats (smallstep/cli#1256) + +### Fixed + +- Parse crlEntryExtensions in CRLs (smallstep/cli#1262) +- PowerShell 5.1 CLI crashes in Windows 11 (smallstep/cli#1257) + +### Notes + +- Skipping 0.27.3 to synchronize with smallstep/certificates + + +## [0.27.2] - 2024年07月18日 + +### Added + +- `console` flag to SSH commands (smallstep/cli#1238) +- Upload FreeBSD build to S3 (smallstep/cli#1239) + + +## [0.27.1] - 2024年07月11日 + +### Fixed + +- Broken release process + + +## [0.27.0] - 2024年07月11日 + +### Changed + +- Makefile: install to /usr/local/bin, not /usr/bin (smallstep/cli#1214) + +### Fixed + +- Set proper JOSE algorithm for Ed25519 keys (smallstep/cli#1208) +- Makefile: usage of install command line flags on MacOS (smallstep/cli#1212) +- Restore operation of '--bundle' flag in certificate inspect (smallstep/cli#1215) +- Fish completion (smallstep/cli#1222) +- Restore operation of inspect CSR from STDIN (smallstep/cli#1232) + +### Security + + +## [0.26.2] - 2024年06月13日 + +### Added + +- Options for auth-params and scopes to OIDC token generator (smallstep/cli#1154) +- --kty, --curve, and --size to ssh commands (login, certificate) (smallstep/cli#1156) +- Stdin input for SSH needs-renewal (smallstep/cli#1157) +- Allow users to define certificate comment in SSH agent (smallstep/cli#1158) +- Add OCSP and CRL support to certificate verify (smallstep/cli#1161) + + +## [0.26.1] - 2024年04月22日 + +### Added + +- Ability to output inspected CSR in PEM format (smallstep/cli#1153) + +### Fixed + +- Allow 'certificate inspect' to parse PEM files containig extraneous data (smallstep/cli#1153) + + +## [v0.26.0] - 2024年03月27日 + +### Added + +- Sending of (an automatically generated) request identifier in the X-Request-Id header (smallstep/cli#1120) + +### Changed + +- Upgrade certinfo (smallstep/cli#1129) +- Upgrade other dependencies + +### Fixed + +- OIDC flows failing using Chrome and other Chromium based browsers (smallstep/cli#1136) + +### Security + +- Upgrade to using cosign v2 for signing artifacts + +## [v0.25.2] - 2024年01月19日 + +### Added + +- Add support for Nebula certificates using ECDSA P-256 (smallstep/cli#1085) + +### Changed + +- Upgrade docker image using Debian to Bookworm (smallstep/cli#1080) +- Upgrade dependencies, including go-jose to v3 (smallstep/cli#1086) + +## [v0.25.1] - 2023年11月28日 + +### Added + +- Add `step crypto rand` command in (smallstep/cli#1054) +- Support for custom TPM device name in `--attestation-uri` flag in (smallstep/cli#1044) + +### Changed + +- Ignore BOM when reading files in (smallstep/cli#1045) +- Upgraded `truststore` to fix installing certificates on certain Linux systems in (smallstep/cli#1053) + +### Fixed + +- Scoop and WinGet releases +- Command completion for `zsh` in (smallstep/cli#1055) + +## [v0.25.0] - 2023年09月26日 + +### Added + +- Add support for provisioner claim `disableSmallstepExtensions` + (smallstep/cli#986) +- Add support for PowerShell plugins on Windows (smallstep/cli#992) +- Create API token using team slug (smallstep/cli#980) +- Detect OIDC tokens issued by Kubernetes (smallstep/cli#953) +- Add support for Smallstep Managed Endpoint X509 extension + (smallstep/cli#989) +- Support signing a certificate for a private key that can only be used for + encryption with the `--skip-csr-signature` flag in `step certificate create`. + Some KMSs restrict key usage to a single type of cryptographic operation. + This blocks RSA decryption keys from being used to sign a CSR for their public + key. Using the `--skip-csr-signature` flag, the public key is used directly + with a certificate template, removing the need for the CSR signature. +- Add all AWS identity document certificates (smallstep/certificates#1510) +- Add SCEP decrypter configuration flags (smallstep/cli#950) +- Add detection of OIDC tokens issued by Kubernetes (smallstep/cli#953) +- Add unversioned release artifacts to build (smallstep/cli#965) + +### Changed + +- Increase PBKDF2 iterations to 600k (smallstep/cli#949) +- `--kms` flag is no longer used for the CA (signing) key for +`step certificate create`. It was replaced by the `--ca-kms` flag +(smallstep/cli#942). +- Hide `step oauth command` on failure (smallstep/cli#993) + +### Fixed + +- Look for Windows plugins with executable extensions + (smallstep/certificates#976) +- Fix empty ca.json with invalid template data (smallstep/certificates#1501) +- Fix interactive prompt on docker builds (smallstep/cli#963) +- `step certificate fingerprint` correctly parse PEM files with non-PEM header + (smallstep/crypto#311) +- `step certificate format` correctly parse PEM files with non-PEM header + (smallstep/cli#1006) +- Fix TOFU flag in `ca provisioner update` (smallstep/cli#941) +- Make `--team` incompatible with `--fingerprint` and `--ca-url` in + `step ca bootstrap (smallstep/cli#1017) + +### Remove + +- Remove automatic creation of the step path (smallstep/certificates#991) + +## [v0.24.4] - 2023年05月11日 + +### Added + +- Documentation for fish completion (smallstep/cli#930) +- `--audience` flag to `step api token` (smallstep/cli#927) + +### Changed + +- Depend on [smallstep/go-attestation](https://github.com/smallstep/go-attestation) instead of [google/go-attestation](https://github.com/google/go-attestation) +- Implementation for parsing CRLs (smallstep/cli#926) + +## [v0.24.3] - 2023年04月14日 + +### Added + +- Storing of certificate chain for TPM keys in TPM storage (smallstep/cli#915) + +### Changed + +- The enrolment URL path used when enrolling with an attestation CA (smallstep/cli#915) + +### Fixed + +- Issue with CLI reference not showing curly braces correctly (smallstep/cli#916) +- Word wrapping for `step api token` example (smallstep/cli#917) + +## [v0.24.2] - 2023年04月14日 + +### Changed + +- Cross-compile Debian docker builds to improve release performance + (smallstep/cli#911). + +### Fixed + +- Fix encrypted PKCS#8 keys used on `step crypto key format` + (smallstep/crypto#216). + +## [v0.24.1] - 2023年04月12日 + +### Fixed + +- Upgrade certificates version (smallstep/cli#910). + +## [v0.24.0] - 2023年04月12日 + +### Added + +- Support for ACME device-attest-01 challenge with TPM 2.0 (smallstep/cli#712). +- Build and release cleanups (smallstep/cli#883, smallstep/cli#884, + smallstep/cli#888, and smallstep/cli#896). +- Release of the smallstep/step-cli:bullseye docker image with CGO and glibc + support (smallstep/cli#885). +- Support for reload using the HUP signal on the test command `step fileserver` + (smallstep/cli#891). +- Support for Azure sovereign clouds (smallstep/cli#872). + +### Fixed + +- Fix the `--insecure` flag when creating RSA keys of less than 2048 bits + (smallstep/cli#878). +- Fix docs for active revocation (smallstep/cli#889) +- Fix signing of X5C tokens with ECDSA P-384 and P-521 keys. +- Fix 404 links in docs (smallstep/cli#907). +- Linting and cleanup changes (smallstep/cli#904 and smallstep/cli#905). + +### Changed + +- Use key fingerprints by default for SSH certificates, and add `--certificate` + flag to print the certificate fingerprint (smallstep/cli#908). + +### Removed + +- Remove `--hugo` flag in `step help` command (smallstep/cli#898). + +## [v0.23.4] - 2023年03月09日 + +### Added + +- Support on `step ca token` for signing JWK, X5C and SSHPOP tokens using a KMS + (smallstep/cli#871). +- debian:bullseye base image (smallstep/cli#861) + +### Changed + +- `step certificate needs-renewal` will only check the leaf certificate by default. + To test the full certificate bundle use the `--bundle` flag. (smallstep/cli#873) +- Change how `step help --markdown` works: It now ouputs "REAME.mdx" instead of "index.md" + +## [v0.23.3] - 2023年03月01日 + +### Fixed + +- Prevent re-use of TCP connections between requests on `step oauth` (smallstep/cli#858). +- Upgrade certinfo with a fix for the YubiKey touch policy information (smallstep/cli#854). +- Upgrade Golang dependencies with reported issues. + +## [v0.23.2] - 2023年02月06日 + +### Added + +- Added support for extended SANs when creating CSRs (smallstep/crypto#168). +- Added check for empty DNS value in `step ca init` (smallstep/cli#815). + +### Changed + +- Improved prompts and error messages in `step ca init` (smallstep/cli#827), + (smallstep/cli#831), (smallstep/cli#839). +- Improved ACME device-attest-01 challenge validation logic (smallstep/cli#837). + +### Fixed + +- Fixed `step ca provisioner add` when CA is not online (smallstep/cli#833). + +## [v0.23.1] - 2023年01月10日 + +### Added + +- Add scope parameter in `step oauth` (smallstep/cli#816). + +### Changed + +- Check for remote configuration API before prompting for admin credentials + (smallstep/cli809). ### Fixed -- Generation of OTT when signing a CSR with URIs. +- Generation of OTT when signing a CSR with URIs (smallstep/cli#799). +- CA certificates path for SLSE with + [smallstep/truststore/#16](https://github.com/smallstep/truststore/pull/16) + (smallstep/cli#818). ## [v0.23.0] - 2022年11月11日 diff --git a/Makefile b/Makefile index 83bdad36..4bb594ae 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,32 @@ +# Run `make bootstrap` to set up your local environment. +# To build using go, use `make build` +# For a binary that's in parity with how our CI system builds, +# run `make goreleaser` to build using GoReleaser Pro. + +# Variables: +# V=1 for verbose output. + +# the name of the executable +BINNAME?=step + +# the build output path +PREFIX?=bin + +# the install path +DESTDIR?=/usr/local/bin + +# GOOS_OVERRIDE="GOOS=linux GOARCH=arm GOARM=6" to change OS and arch +GOOS_OVERRIDE?= + +# CGO_OVERRIDE="CGO_ENABLED=1" to enable CGO +CGO_OVERRIDE?=CGO_ENABLED=0 + +# which build id in .goreleaser.yml to build +GORELEASER_BUILD_ID?=default + +# all go files +SRC=$(shell find . -type f -name '*.go' -or -name go.mod -or -name go.sum) + all: lint test build ci: test build @@ -8,17 +37,7 @@ ci: test build # Determine the type of `push` and `version` ################################################# -# If TRAVIS_TAG is set then we know this ref has been tagged. -ifdef TRAVIS_TAG -VERSION ?= $(TRAVIS_TAG) -NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc) - ifeq ($(NOT_RC),) -PUSHTYPE := release-candidate - else -PUSHTYPE := release - endif -# GITHUB Actions -else ifdef GITHUB_REF +ifdef GITHUB_REF VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///') NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc) ifeq ($(NOT_RC),) @@ -30,82 +49,187 @@ else VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev") # If we are not in an active git dir then try reading the version from .VERSION. # .VERSION contains a slug populated by `git archive`. -VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION)) - ifeq ($(TRAVIS_BRANCH),master) -PUSHTYPE := master - else +VERSION := $(or $(VERSION),$(shell make/version.sh .VERSION)) PUSHTYPE := branch - endif endif VERSION := $(shell echo $(VERSION) | sed 's/^v//') ifdef V -$(info TRAVIS_TAG is $(TRAVIS_TAG)) $(info GITHUB_REF is $(GITHUB_REF)) $(info VERSION is $(VERSION)) $(info PUSHTYPE is $(PUSHTYPE)) endif -include make/common.mk +DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC') +ifdef DEBUG + LDFLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' + GCFLAGS := -gcflags "all=-N -l" +else + LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' + GCFLAGS := +endif + +Q=$(if $V,,@) +OUTPUT_ROOT=output/ + +ifeq ($(OS),Windows_NT) + HOSTOS=Windows +else + HOSTOS=$(shell uname) +endif + +HOSTARCH=$(shell go env GOHOSTARCH) +ifeq ($(HOSTARCH),amd64) + HOSTARCH=x86_64 +endif + +GORELEASER_PRO_URL=https://github.com/goreleaser/goreleaser-pro/releases/latest/download/goreleaser-pro_$(HOSTOS)_$(HOSTARCH).tar.gz + +# Determine the hooks to skip. When using GoReleaser OSS with a Pro config, specifying "after" +# to be skipped results in an error. When using GoReleaser Pro running the "goreleaser-local" +# target both "post-hooks" and "after" are required to skip the upload to GCP. The logic below +# checks the GoReleaser binary to be Pro or not, and then sets the steps to skip accordingly. +# It's possible this is a GoReleaser bug for the case where a Pro config is used with GoReleaser +# OSS. +GORELEASER_OSS_SKIP=post-hooks +GORELEASER_PRO_SKIP=post-hooks,after +GORELEASER_SKIP=$(if $(filter true,$(shell goreleaser --version | grep -q goreleaser-pro && echo true || echo false)),$(GORELEASER_PRO_SKIP),$(GORELEASER_OSS_SKIP)) + +.PHONY: all + +######################################### +# Bootstrapping +######################################### +TMPDIR := $(shell mktemp -d) +bootstra%: GOPATH=$(shell go env GOPATH) +bootstra%: + $Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest + $Q go install golang.org/x/vuln/cmd/govulncheck@latest + $Q go install gotest.tools/gotestsum@latest + $Q go install golang.org/x/tools/cmd/goimports@latest + @echo "Installing GoReleaser Pro into $(GOPATH)/bin" + $Q curl -o $(TMPDIR)/goreleaser.tar.gz -L $(GORELEASER_PRO_URL) + $Q ls $(TMPDIR) + $Q tar xvzf $(TMPDIR)/goreleaser.tar.gz -C $(TMPDIR) + $Q cp $(TMPDIR)/goreleaser $(GOPATH)/bin + +.PHONY: bootstra% + +######################################### +# Build +######################################### + +build: $(PREFIX)/$(BINNAME) + @echo "Build Complete!" + +$(PREFIX)/$(BINNAME): $(SRC) + $Q mkdir -p $(PREFIX) + $Q $(GOOS_OVERRIDE) $(CGO_OVERRIDE) go build \ + -v \ + -o $(PREFIX)/$(BINNAME) \ + $(GCFLAGS) $(LDFLAGS) \ + github.com/smallstep/cli/cmd/step + +goreleaser: + $Q mkdir -p $(PREFIX) + $Q $(GOOS_OVERRIDE) $(CGO_OVERRIDE) DEBUG=$(DEBUG) goreleaser build \ + --id $(GORELEASER_BUILD_ID) \ + --snapshot \ + --single-target \ + --clean \ + --skip=$(GORELEASER_SKIP) \ + --output $(PREFIX)/$(BINNAME) + +.PHONY: build goreleaser + + +######################################### +# Test +######################################### + +test: + $Q $(CGO_OVERRIDE) $(GOFLAGS) gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./... + +race: + $Q $(CGO_OVERRIDE) $(GOFLAGS) gotestsum -- -race ./... + +.PHONY: test race + +######################################### +# Linting +######################################### + +fmt: + $Q goimports -local github.com/golangci/golangci-lint -l -w $(SRC) + +lint: golint govulncheck + +golint: SHELL:=/bin/bash +golint: + $Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m + +govulncheck: + $Q govulncheck ./... + +.PHONY: fmt lint golint govulncheck + +######################################### +# Install +######################################### + +install: $(PREFIX)/$(BINNAME) + $Q mkdir -p $(DESTDIR)/ + $Q install $(PREFIX)/$(BINNAME) $(DESTDIR)/$(BINNAME) + +uninstall: + $Q rm -f $(DESTDIR)/$(BINNAME) + +.PHONY: install uninstall + +######################################### +# Clean +######################################### + +clean: + $Q rm -f $(PREFIX)/$(BINNAME) + $Q rm -rf dist + +.PHONY: clean ################################################# # Build statically compiled step binary for various operating systems ################################################# BINARY_OUTPUT=$(OUTPUT_ROOT)binary/ -RELEASE=./.releases define BUNDLE_MAKE # $(1) -- Go Operating System (e.g. linux, darwin, windows, etc.) # $(2) -- Go Architecture (e.g. amd64, arm, arm64, etc.) # $(3) -- Go ARM architectural family (e.g. 7, 8, etc.) # $(4) -- Parent directory for executables generated by 'make'. - $(q) GOOS_OVERRIDE='GOOS=$(1) GOARCH=$(2) GOARM=$(3)' PREFIX=$(4) make $(4)bin/step + $Q GOOS_OVERRIDE='GOOS=$(1) GOARCH=$(2) GOARM=$(3)' PREFIX=$(4) make $(4)/$(BINNAME) endef binary-linux-amd64: - $(call BUNDLE_MAKE,linux,amd64,,$(BINARY_OUTPUT)linux-amd64/) + $(call BUNDLE_MAKE,linux,amd64,,$(BINARY_OUTPUT)linux-amd64) binary-linux-arm64: - $(call BUNDLE_MAKE,linux,arm64,,$(BINARY_OUTPUT)linux-arm64/) + $(call BUNDLE_MAKE,linux,arm64,,$(BINARY_OUTPUT)linux-arm64) binary-linux-armv7: - $(call BUNDLE_MAKE,linux,arm,7,$(BINARY_OUTPUT)linux-armv7/) + $(call BUNDLE_MAKE,linux,arm,7,$(BINARY_OUTPUT)linux-armv7) binary-linux-mips: - $(call BUNDLE_MAKE,linux,mips,,$(BINARY_OUTPUT)linux-mips/) + $(call BUNDLE_MAKE,linux,mips,,$(BINARY_OUTPUT)linux-mips) binary-darwin-amd64: - $(call BUNDLE_MAKE,darwin,amd64,,$(BINARY_OUTPUT)darwin-amd64/) + $(call BUNDLE_MAKE,darwin,amd64,,$(BINARY_OUTPUT)darwin-amd64) binary-darwin-arm64: - $(call BUNDLE_MAKE,darwin,amd64,,$(BINARY_OUTPUT)darwin-arm64/) + $(call BUNDLE_MAKE,darwin,arm64,,$(BINARY_OUTPUT)darwin-arm64) binary-windows-amd64: - $(call BUNDLE_MAKE,windows,amd64,,$(BINARY_OUTPUT)windows-amd64/) - -define BUNDLE - # $(1) -- Format output as .ZIP archive, rather than .tar.gzip (for older windows architecture) - # $(2) -- Binary Output Dir Name - # $(3) -- Step Platform Name - # $(4) -- Step Binary Architecture - # $(5) -- Step Binary Name (For Windows Compatibility) - $(q) ./make/bundle.sh $(1) "$(BINARY_OUTPUT)$(2)" "$(RELEASE)" "$(VERSION)" "$(3)" "$(4)" "$(5)" -endef - -bundle-linux: binary-linux-amd64 binary-linux-arm64 binary-linux-armv7 binary-linux-mips - $(call BUNDLE,,linux-amd64,linux,amd64,step) - $(call BUNDLE,,linux-arm64,linux,arm64,step) - $(call BUNDLE,,linux-armv7,linux,armv7,step) - $(call BUNDLE,,linux-mips,linux,mips,step) - -bundle-darwin: binary-darwin-amd64 binary-darwin-arm64 - $(call BUNDLE,,darwin-amd64,darwin,amd64,step) - $(call BUNDLE,,darwin-arm64,darwin,arm64,step) - -bundle-windows: binary-windows-amd64 - $(call BUNDLE,,windows-amd64,windows,amd64,step.exe) - $(call BUNDLE,--zip,windows-amd64,windows,amd64,step.exe) + $(call BUNDLE_MAKE,windows,amd64,,$(BINARY_OUTPUT)windows-amd64) -.PHONY: binary-linux-amd64 binary-linux-arm64 binary-linux-armv7 binary-linux-mips binary-darwin-amd64 binary-darwin-arm64 binary-windows-amd64 bundle-linux bundle-darwin bundle-windows +.PHONY: binary-linux-amd64 binary-linux-arm64 binary-linux-armv7 binary-linux-mips binary-darwin-amd64 binary-darwin-arm64 binary-windows-amd64 diff --git a/README.md b/README.md index 09f2921d..14550b58 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ [![Twitter followers](https://img.shields.io/twitter/follow/smallsteplabs.svg?label=Follow&style=social)](https://twitter.com/intent/follow?screen_name=smallsteplabs) `step` is an easy-to-use CLI tool for building, operating, and automating Public Key Infrastructure (PKI) systems and workflows. -It's the client counterpart to the [`step-ca` online Certificate Authority (CA)](https://github.com/smallstep/certificates). +It's also a client for the [`step-ca` online Certificate Authority (CA)](https://github.com/smallstep/certificates) server. You can use it for many common crypto and X.509 operations—either independently, or with an online CA. -**Questions? Ask us on [GitHub Discussions](https://github.com/smallstep/certificates/discussions) or [Discord](https://bit.ly/step-discord).** +**Questions? Ask us on [GitHub Discussions](https://github.com/smallstep/certificates/discussions) or [Discord](https://u.step.sm/discord).** [Website](https://smallstep.com) | [Documentation](https://smallstep.com/docs/step-cli) | @@ -54,7 +54,7 @@ Step CLI's command groups illustrate its wide-ranging uses: - [Generate and verify](https://smallstep.com/docs/step-cli/reference/crypto/otp/) TOTP tokens for multi-factor authentication (MFA) - Work with [NaCl](https://nacl.cr.yp.to/)'s high-speed tools for encryption and signing - - [Apply key derivation functions](https://smallstep.com/docs/step-cli/reference/crypto/kdf/) (KDFs) and [verify passwords](https://smallstep.com/docs/step-cli/reference/crypto/kdf/compare/) using `scrypt`, `bcrypt`, and `argo2` + - [Apply key derivation functions](https://smallstep.com/docs/step-cli/reference/crypto/kdf/) (KDFs) and [verify passwords](https://smallstep.com/docs/step-cli/reference/crypto/kdf/compare/) using `scrypt`, `bcrypt`, and `argon2` - Generate and check [file hashes](https://smallstep.com/docs/step-cli/reference/crypto/hash/) - [`step oauth`](https://smallstep.com/docs/step-cli/reference/oauth/): Add an OAuth 2.0 single sign-on flow to any CLI application. @@ -78,10 +78,32 @@ Here's a quick example, combining `step oauth` and `step crypto` to get and veri ![Animated terminal showing step in practice](https://smallstep.com/images/blog/2018-08-07-unfurl.gif) +## Plugins + +A plugin is an executable file named using the format `step--plugin`. +Plugins must be available in your `$PATH` or in the `$STEPPATH/plugins` +directory (that's `$HOME/.step/plugins`, by default). + +When you run `step `, the CLI will automatically execute the corresponding +plugin, if found. + +Some known plugins include: + +- [**step-kms-plugin**](https://github.com/smallstep/step-kms-plugin): Manage +keys and certificates stored in a KMS, including HSMs, TPMs, YubiKeys, the macOS +Keychain, and cloud KMSs. +- [**step-kmsproxy-plugin**](https://github.com/orbit-online/step-kmsproxy-plugin): +Provides an HSM/KMS-backed authenticating proxy for mTLS services. Thanks to +[@andsens](https://github.com/andsens) for creating and maintaining this plugin! + +`step-kms-plugin` is also integrated directly into `step` to create +certificates, generate CSRs, sign tokens, and more using KMS-backed keys. + ## Community -* Connect with `step` users on [GitHub Discussions](https://github.com/smallstep/certificates/discussions) or [Discord](https://bit.ly/step-discord) +* Connect with `step` users on [GitHub Discussions](https://github.com/smallstep/certificates/discussions) or [Discord](https://u.step.sm/discord) * [Open an issue](https://github.com/smallstep/cli/issues/new/choose) and tell us what features you'd like to see +* [Contribute](./docs/CONTRIBUTING.md) to the `step` codebase * [Follow Smallstep on Twitter](https://twitter.com/smallsteplabs) ## Further Reading diff --git a/cmd/step/main.go b/cmd/step/main.go index 4aec0182..8dc5e7b1 100644 --- a/cmd/step/main.go +++ b/cmd/step/main.go @@ -1,45 +1,10 @@ package main import ( - "errors" - "fmt" - "math/rand" - "os" - "reflect" - "regexp" - "strings" - "time" - "github.com/smallstep/certificates/ca" - "github.com/smallstep/cli/command/version" - "github.com/smallstep/cli/internal/plugin" - "github.com/smallstep/cli/usage" - "github.com/smallstep/cli/utils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/step" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/pemutil" - - // Enabled commands - _ "github.com/smallstep/cli/command/base64" - _ "github.com/smallstep/cli/command/beta" - _ "github.com/smallstep/cli/command/ca" - _ "github.com/smallstep/cli/command/certificate" - _ "github.com/smallstep/cli/command/completion" - _ "github.com/smallstep/cli/command/context" - _ "github.com/smallstep/cli/command/crl" - _ "github.com/smallstep/cli/command/crypto" - _ "github.com/smallstep/cli/command/fileserver" - _ "github.com/smallstep/cli/command/oauth" - _ "github.com/smallstep/cli/command/path" - _ "github.com/smallstep/cli/command/ssh" + "github.com/smallstep/cli-utils/step" - // Enabled cas interfaces. - _ "github.com/smallstep/certificates/cas/cloudcas" - _ "github.com/smallstep/certificates/cas/softcas" - _ "github.com/smallstep/certificates/cas/stepcas" + "github.com/smallstep/cli/internal/cmd" ) // Version is set by an LDFLAG at build time representing the git tag or commit @@ -50,130 +15,15 @@ var Version = "N/A" // the time of build var BuildTime = "N/A" +// AppName is the name of the binary. Defaults to "step" if not set. +var AppName = "" + func init() { step.Set("Smallstep CLI", Version, BuildTime) ca.UserAgent = step.Version() - rand.Seed(time.Now().UnixNano()) + cmd.SetName(AppName) } func main() { - defer panicHandler() - // Override global framework components - cli.VersionPrinter = func(c *cli.Context) { - version.Command(c) - } - cli.AppHelpTemplate = usage.AppHelpTemplate - cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate - cli.CommandHelpTemplate = usage.CommandHelpTemplate - cli.HelpPrinter = usage.HelpPrinter - cli.FlagNamePrefixer = usage.FlagNamePrefixer - cli.FlagStringer = stringifyFlag - - // Configure cli app - app := cli.NewApp() - app.Name = "step" - app.HelpName = "step" - app.Usage = "plumbing for distributed systems" - app.Version = step.Version() - app.Commands = command.Retrieve() - app.Flags = append(app.Flags, cli.HelpFlag) - app.EnableBashCompletion = true - app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year()) - - // Flag of custom configuration flag - app.Flags = append(app.Flags, cli.StringFlag{ - Name: "config", - Usage: "path to the config file to use for CLI flags", - }) - - // Action runs on `step` or `step ` if the command is not enabled. - app.Action = func(ctx *cli.Context) error { - args := ctx.Args() - if name := args.First(); name != "" { - if file, err := plugin.LookPath(name); err == nil { - return plugin.Run(ctx, file) - } - if u := plugin.GetURL(name); u != "" { - //nolint:stylecheck // this is a top level error - capitalization is ok - return fmt.Errorf("The plugin %q is not it in your system.\nDownload it from %s", name, u) - } - return cli.ShowCommandHelp(ctx, name) - } - return cli.ShowAppHelp(ctx) - } - - // All non-successful output should be written to stderr - app.Writer = os.Stdout - app.ErrWriter = os.Stderr - - // Define default file writers and prompters for go.step.sm/crypto - pemutil.WriteFile = utils.WriteFile - pemutil.PromptPassword = func(msg string) ([]byte, error) { - return ui.PromptPassword(msg) - } - jose.PromptPassword = func(msg string) ([]byte, error) { - return ui.PromptPassword(msg) - } - - if err := app.Run(os.Args); err != nil { - var messenger interface { - Message() string - } - if errors.As(err, &messenger) { - if os.Getenv("STEPDEBUG") == "1" { - fmt.Fprintf(os.Stderr, "%+v\n\n%s", err, messenger.Message()) - } else { - fmt.Fprintln(os.Stderr, messenger.Message()) - fmt.Fprintln(os.Stderr, "Re-run with STEPDEBUG=1 for more info.") - } - } else { - if os.Getenv("STEPDEBUG") == "1" { - fmt.Fprintf(os.Stderr, "%+v\n", err) - } else { - fmt.Fprintln(os.Stderr, err) - } - } - //nolint:gocritic // ignore exitAfterDefer error because the defer is required for recovery. - os.Exit(1) - } -} - -func panicHandler() { - if r := recover(); r != nil { - if os.Getenv("STEPDEBUG") == "1" { - fmt.Fprintf(os.Stderr, "%s\n", step.Version()) - fmt.Fprintf(os.Stderr, "Release Date: %s\n\n", step.ReleaseDate()) - panic(r) - } else { - fmt.Fprintln(os.Stderr, "Something unexpected happened.") - fmt.Fprintln(os.Stderr, "If you want to help us debug the problem, please run:") - fmt.Fprintf(os.Stderr, "STEPDEBUG=1 %s\n", strings.Join(os.Args, " ")) - fmt.Fprintln(os.Stderr, "and send the output to info@smallstep.com") - os.Exit(2) - } - } -} - -func flagValue(f cli.Flag) reflect.Value { - fv := reflect.ValueOf(f) - for fv.Kind() == reflect.Ptr { - fv = reflect.Indirect(fv) - } - return fv -} - -var placeholderString = regexp.MustCompile(`<.*?>`) - -func stringifyFlag(f cli.Flag) string { - fv := flagValue(f) - usg := fv.FieldByName("Usage").String() - placeholder := placeholderString.FindString(usg) - if placeholder == "" { - switch f.(type) { - case cli.BoolFlag, cli.BoolTFlag: - default: - placeholder = "" - } - } - return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usg + cmd.Run() } diff --git a/command/README.md b/command/README.md index 57945372..0e26c9fb 100644 --- a/command/README.md +++ b/command/README.md @@ -12,7 +12,7 @@ should exist within its own package if possible. For example, `version` and Any package used by a command but does not contain explicit business logic directly related to the command should exist in the top-level of this repository. For example, the `github.com/smallstep/cli/flags` and -`go.step.sm/cli-utils/errs` package are used by many different commands and +`github.com/smallstep/cli-utils/errs` package are used by many different commands and contain functionality for defining flags and creating/manipulating errors. ### Adding a Command @@ -68,10 +68,10 @@ and thus registered with the `smallstep/cli/command`. There are three packages which contain functionality to make writing commands easier: -- `github.com/smallstep/cli/usage` - `github.com/smallstep/cli/flags` - `github.com/smallstep/cli/prompts` -- `go.step.sm/cli-utils/errs` +- `github.com/smallstep/cli-utils/errs` +- `github.com/smallstep/cli-utils/usage` The usage package is used to extend the default documentation provided by `urfave/cli` by enabling us to document arguments, whether they are optional or diff --git a/command/api/api.go b/command/api/api.go new file mode 100644 index 00000000..457cdeb1 --- /dev/null +++ b/command/api/api.go @@ -0,0 +1,25 @@ +package api + +import ( + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/command" + + "github.com/smallstep/cli/command/api/token" +) + +func init() { + cmd := cli.Command{ + Hidden: true, + Name: "api", + Usage: "authenticate to the Smallstep API", + UsageText: "**step api** [arguments] [global-flags] [subcommand-flags]", + Description: `**step api** provides commands for connecting to the Smallstep API. +`, + Subcommands: cli.Commands{ + token.Command(), + }, + } + + command.Register(cmd) +} diff --git a/command/api/token/create.go b/command/api/token/create.go new file mode 100644 index 00000000..dc270533 --- /dev/null +++ b/command/api/token/create.go @@ -0,0 +1,145 @@ +package token + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "path" + + "github.com/google/uuid" + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" +) + +func createCommand() cli.Command { + return cli.Command{ + Name: "create", + Action: cli.ActionFunc(createAction), + Usage: "create a new token", + UsageText: `**step api token create** +[**--api-url**=] [**--audience**=] +`, + Flags: []cli.Flag{ + apiURLFlag, + audienceFlag, + }, + Description: `**step ca api token create** creates a new token for connecting to the Smallstep API. + +## POSITIONAL ARGUMENTS + + +: UUID or slug of the team the API token will be issued for. This is available in the Smallstep dashboard. + + +: File to read the certificate (PEM format). This certificate must be signed by a trusted root configured in the Smallstep dashboard. + + +: File to read the private key (PEM format). + +## EXAMPLES +Use a certificate to get a new API token: +''' +$ step api token create ff98be70-7cc3-4df5-a5db-37f5d3c96e23 internal.crt internal.key +''' + +Get a token using the team slug: +''' +$ step api token create teamfoo internal.crt internal.key +''' +`, + } +} + +type createTokenReq struct { + TeamID string `json:"teamID"` + TeamSlug string `json:"teamSlug"` + Bundle [][]byte `json:"bundle"` + Audience string `json:"audience,omitempty"` +} + +type createTokenResp struct { + Token string `json:"token"` + Message string `json:"message"` +} + +func createAction(ctx *cli.Context) (err error) { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + args := ctx.Args() + + teamID := args.Get(0) + crtFile := args.Get(1) + keyFile := args.Get(2) + + parsedURL, err := url.Parse(ctx.String("api-url")) + if err != nil { + return err + } + parsedURL.Path = path.Join(parsedURL.Path, "api/auth") + apiURL := parsedURL.String() + + clientCert, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + return err + } + b := &bytes.Buffer{} + r := &createTokenReq{ + Bundle: clientCert.Certificate, + Audience: ctx.String("audience"), + } + if err := uuid.Validate(teamID); err != nil { + r.TeamSlug = teamID + } else { + r.TeamID = teamID + } + err = json.NewEncoder(b).Encode(r) + if err != nil { + return err + } + + post, err := http.NewRequest("POST", apiURL, b) + if err != nil { + return err + } + post.Header.Set("Content-Type", "application/json") + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &clientCert, nil + }, + MinVersion: tls.VersionTLS12, + } + client := http.Client{ + Transport: transport, + } + resp, err := client.Do(post) // #nosec G704 -- request depends on configuration + if err != nil { + return err + } + defer resp.Body.Close() + + respBody := &createTokenResp{} + if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil { + return err + } + if resp.StatusCode != 201 { + if respBody.Message != "" { + return errors.New(respBody.Message) + } + return fmt.Errorf("failed to create token: %d", resp.StatusCode) + } + + // Print message to stderr for humans and token to stdout for scripts + ui.PrintSelected("Token successfully created", "") + fmt.Println(respBody.Token) + + return nil +} diff --git a/command/api/token/token.go b/command/api/token/token.go new file mode 100644 index 00000000..1925e63c --- /dev/null +++ b/command/api/token/token.go @@ -0,0 +1,33 @@ +package token + +import ( + "github.com/urfave/cli" +) + +// Command returns the token subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "token", + Usage: "create tokens for connecting to the Smallstep API", + UsageText: "step api token [arguments] [global-flags] [subcommand-flags]", + Subcommands: cli.Commands{ + createCommand(), + }, + Description: `**step api token** command group provides commands for creating the +tokens required to connect to the Smallstep API. +`, + } +} + +// common flags +var ( + apiURLFlag = cli.StringFlag{ + Name: "api-url", + Usage: "URL where the Smallstep API can be found", + Value: "https://gateway.smallstep.com", + } + audienceFlag = cli.StringFlag{ + Name: "audience", + Usage: "Request a token for an audience other than the API Gateway", + } +) diff --git a/command/base64/base64.go b/command/base64/base64.go index 17e7495f..32b8c1ea 100644 --- a/command/base64/base64.go +++ b/command/base64/base64.go @@ -8,9 +8,11 @@ import ( "strings" "github.com/pkg/errors" - "github.com/smallstep/cli/utils" "github.com/urfave/cli" - "go.step.sm/cli-utils/command" + + "github.com/smallstep/cli-utils/command" + + "github.com/smallstep/cli/utils" ) func init() { diff --git a/command/beta/beta.go b/command/beta/beta.go index dc2c827a..5b507378 100644 --- a/command/beta/beta.go +++ b/command/beta/beta.go @@ -1,9 +1,11 @@ package beta import ( - "github.com/smallstep/cli/command/ca" "github.com/urfave/cli" - "go.step.sm/cli-utils/command" + + "github.com/smallstep/cli-utils/command" + + "github.com/smallstep/cli/command/ca" ) // init creates and registers the ca command diff --git a/command/ca/acme/eab/add.go b/command/ca/acme/eab/add.go index ead3e379..2fe70c79 100644 --- a/command/ca/acme/eab/add.go +++ b/command/ca/acme/eab/add.go @@ -7,9 +7,9 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - adminAPI "github.com/smallstep/certificates/authority/admin/api" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" ) diff --git a/command/ca/acme/eab/eab.go b/command/ca/acme/eab/eab.go index ef9102a3..b989482b 100644 --- a/command/ca/acme/eab/eab.go +++ b/command/ca/acme/eab/eab.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" - "go.step.sm/linkedca" + "github.com/smallstep/linkedca" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/ca" @@ -25,7 +25,7 @@ type cliEAK struct { account string } -func toCLI(ctx *cli.Context, client *ca.AdminClient, eak *linkedca.EABKey) *cliEAK { +func toCLI(_ *cli.Context, _ *ca.AdminClient, eak *linkedca.EABKey) *cliEAK { boundAt := "" if !eak.BoundAt.AsTime().IsZero() { boundAt = eak.BoundAt.AsTime().Format("2006-01-02 15:04:05 -07:00") @@ -52,7 +52,7 @@ func Command() cli.Command { addCommand(), removeCommand(), }, - Description: `**step ca acme eab** command group provides facilities for managing ACME + Description: `**step ca acme eab** command group provides facilities for managing ACME External Account Binding Keys. ## EXAMPLES diff --git a/command/ca/acme/eab/list.go b/command/ca/acme/eab/list.go index 0bc3aeef..51e1bfe5 100644 --- a/command/ca/acme/eab/list.go +++ b/command/ca/acme/eab/list.go @@ -5,13 +5,16 @@ import ( "io" "os" "os/exec" + "strings" "github.com/pkg/errors" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cast" "github.com/smallstep/cli/utils/cautils" ) @@ -97,8 +100,16 @@ func listAction(ctx *cli.Context) (err error) { // prepare the $PAGER command to run when not disabled and when available pager := os.Getenv("PAGER") + if strings.ContainsAny(pager, " \t\n;&|") { + return errors.New("invalid PAGER environment value") + } + + if _, err := exec.LookPath(pager); err != nil { + return fmt.Errorf("invalid PAGER environment value: %w", err) + } + if usePager && pager != "" { - cmd = exec.Command(pager) + cmd = exec.Command(pager) // #nosec G702 -- $PAGER is intended to be provided by users; basic validation applied var err error out, err = cmd.StdinPipe() if err != nil { @@ -122,7 +133,7 @@ func listAction(ctx *cli.Context) (err error) { startedPager := false for { - options := []ca.AdminOption{ca.WithAdminCursor(cursor), ca.WithAdminLimit(int(limit))} + options := []ca.AdminOption{ca.WithAdminCursor(cursor), ca.WithAdminLimit(cast.Int(limit))} eaksResponse, err := client.GetExternalAccountKeysPaginate(provisioner, reference, options...) if err != nil { return errors.Wrap(notImplemented(err), "error retrieving ACME EAB keys") diff --git a/command/ca/acme/eab/remove.go b/command/ca/acme/eab/remove.go index 655c1be4..857a4df1 100644 --- a/command/ca/acme/eab/remove.go +++ b/command/ca/acme/eab/remove.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" diff --git a/command/ca/acme/eab/sigchild.go b/command/ca/acme/eab/sigchild.go index 7f0e5715..f2e55f12 100644 --- a/command/ca/acme/eab/sigchild.go +++ b/command/ca/acme/eab/sigchild.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package eab diff --git a/command/ca/acme/eab/sigchild_windows.go b/command/ca/acme/eab/sigchild_windows.go index 4398a249..7c2edccc 100644 --- a/command/ca/acme/eab/sigchild_windows.go +++ b/command/ca/acme/eab/sigchild_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package eab diff --git a/command/ca/admin/add.go b/command/ca/admin/add.go index a7f22d18..5ccdee6e 100644 --- a/command/ca/admin/add.go +++ b/command/ca/admin/add.go @@ -5,12 +5,14 @@ import ( "os" "text/tabwriter" + "github.com/urfave/cli" + adminAPI "github.com/smallstep/certificates/authority/admin/api" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) func addCommand() cli.Command { diff --git a/command/ca/admin/admin.go b/command/ca/admin/admin.go index e76459d4..73944753 100644 --- a/command/ca/admin/admin.go +++ b/command/ca/admin/admin.go @@ -4,12 +4,12 @@ import ( "errors" "fmt" - "github.com/smallstep/certificates/ca" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/linkedca" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + "github.com/smallstep/linkedca" ) // Command returns the jwk subcommand. @@ -71,7 +71,7 @@ type cliAdmin struct { ProvisionerType string } -func toCLI(ctx *cli.Context, client *ca.AdminClient, adm *linkedca.Admin) (*cliAdmin, error) { +func toCLI(_ *cli.Context, client *ca.AdminClient, adm *linkedca.Admin) (*cliAdmin, error) { p, err := client.GetProvisioner(ca.WithProvisionerID(adm.ProvisionerId)) if err != nil { return nil, err diff --git a/command/ca/admin/list.go b/command/ca/admin/list.go index e3a3e7e1..c3e33a29 100644 --- a/command/ca/admin/list.go +++ b/command/ca/admin/list.go @@ -5,11 +5,13 @@ import ( "os" "text/tabwriter" + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) func listCommand() cli.Command { @@ -97,7 +99,7 @@ func listAction(ctx *cli.Context) (err error) { if isNotSuperAdmin && a.Type == linkedca.Admin_SUPER_ADMIN { return false } - if len(provName)> 0 && a.ProvisionerName != provName { + if provName != "" && a.ProvisionerName != provName { return false } return true diff --git a/command/ca/admin/remove.go b/command/ca/admin/remove.go index d8eedd36..40caf5d2 100644 --- a/command/ca/admin/remove.go +++ b/command/ca/admin/remove.go @@ -1,10 +1,12 @@ package admin import ( + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) func removeCommand() cli.Command { diff --git a/command/ca/admin/update.go b/command/ca/admin/update.go index ad8f7820..e01d8f2e 100644 --- a/command/ca/admin/update.go +++ b/command/ca/admin/update.go @@ -5,12 +5,14 @@ import ( "os" "text/tabwriter" + "github.com/urfave/cli" + adminAPI "github.com/smallstep/certificates/authority/admin/api" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) func updateCommand() cli.Command { diff --git a/command/ca/bootstrap.go b/command/ca/bootstrap.go index 6a057335..1d28f3b9 100644 --- a/command/ca/bootstrap.go +++ b/command/ca/bootstrap.go @@ -3,11 +3,13 @@ package ca import ( "strings" + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" ) func bootstrapCommand() cli.Command { @@ -34,13 +36,13 @@ After the bootstrap, ca commands do not need to specify the flags Bootstrap using the CA url and a fingerprint: ''' -$ step ca bootstrap --ca-url https://ca.example.org \ +$ step ca bootstrap --ca-url https://ca.example.com \ --fingerprint d9d0978692f1c7cc791f5c343ce98771900721405e834cd27b9502cc719f5097 ''' Bootstrap and install the root certificate ''' -$ step ca bootstrap --ca-url https://ca.example.org \ +$ step ca bootstrap --ca-url https://ca.example.com \ --fingerprint d9d0978692f1c7cc791f5c343ce98771900721405e834cd27b9502cc719f5097 \ --install ''' @@ -53,19 +55,19 @@ $ step ca bootstrap --team superteam To use team IDs in your own environment, you'll need an HTTP(S) server serving a JSON file: ''' -{"url":"https://ca.example.org","fingerprint":"d9d0978692f1c7cc791f5c343ce98771900721405e834cd27b9502cc719f5097"} +{"url":"https://ca.example.com","fingerprint":"d9d0978692f1c7cc791f5c343ce98771900721405e834cd27b9502cc719f5097"} ''' -Then, this command will look for the file at https://config.example.org/superteam: +Then, this command will look for the file at https://config.example.com/superteam: ''' -$ step ca bootstrap --team superteam --team-url https://config.example.org/ +$ step ca bootstrap --team superteam --team-url https://config.example.com/ '''`, Flags: []cli.Flag{ flags.CaURL, fingerprintFlag, cli.BoolFlag{ Name: "install", - Usage: "Install the root certificate into the system truststore.", + Usage: "Install the root certificate into the system's default trust store.", }, flags.Team, flags.TeamAuthority, @@ -90,6 +92,10 @@ func bootstrapAction(ctx *cli.Context) error { teamAuthority := ctx.String("team-authority") switch { + case team != "" && caURL != "": + return errs.IncompatibleFlagWithFlag(ctx, "team", "ca-url") + case team != "" && fingerprint != "": + return errs.IncompatibleFlagWithFlag(ctx, "team", "fingerprint") case team != "" && teamAuthority != "": return cautils.BootstrapTeamAuthority(ctx, team, teamAuthority) case team != "": diff --git a/command/ca/ca.go b/command/ca/ca.go index df2da3fc..bd5e68c5 100644 --- a/command/ca/ca.go +++ b/command/ca/ca.go @@ -1,12 +1,14 @@ package ca import ( + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli/command/ca/acme" "github.com/smallstep/cli/command/ca/admin" "github.com/smallstep/cli/command/ca/policy" "github.com/smallstep/cli/command/ca/provisioner" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" ) // init creates and registers the ca command @@ -137,11 +139,6 @@ location being served by an existing fileserver in order to respond to ACME challenge validation requests.`, } - consoleFlag = cli.BoolFlag{ - Name: "console", - Usage: "Complete the flow while remaining inside the terminal", - } - fingerprintFlag = cli.StringFlag{ Name: "fingerprint", Usage: "The of the targeted root certificate.", diff --git a/command/ca/certificate.go b/command/ca/certificate.go index d6214107..74ea4db9 100644 --- a/command/ca/certificate.go +++ b/command/ca/certificate.go @@ -1,17 +1,21 @@ package ca import ( + "path/filepath" "strings" "github.com/pkg/errors" + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/step" + "github.com/smallstep/cli-utils/ui" + "go.step.sm/crypto/pemutil" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/token" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/pemutil" ) func certificateCommand() cli.Command { @@ -24,7 +28,7 @@ func certificateCommand() cli.Command { [**--not-before**=] [**--not-after**=] [**--san**=] [**--set**=] [**--set-file**=] [**--acme**=] [**--standalone**] [**--webroot**=] -[**--contact**=] [**--http-listen**=
] [**--bundle**] +[**--contact**=] [**--http-listen**=
] [**--kty**=] [**--curve**=] [**--size**=] [**--console**] [**--x5c-cert**=] [**--x5c-key**=] [**--k8ssa-token-path**=] [**--offline**] [**--password-file**] [**--ca-url**=] [**--root**=] @@ -105,6 +109,13 @@ Request a new certificate with an X5C provisioner: $ step ca certificate foo.internal foo.crt foo.key --x5c-cert x5c.cert --x5c-key x5c.key ''' +Request a new certificate with an X5C provisioner using a certificate and private key stored on a YubiKey: +''' +$ step ca certificate joe@example.com joe.crt joe.key \ + --x5c-cert yubikey:slot-id=9a \ + --x5c-key 'yubikey:slot-id=9a?pin=value=123456' +''' + **Certificate Templates** - With a provisioner configured with a custom template we can use the **--set** flag to pass user variables: ''' @@ -153,6 +164,24 @@ $ step ca certificate foo.internal foo.crt foo.key \ that should be authorized. Use the '--san' flag multiple times to configure multiple SANs. The '--san' flag and the '--token' flag are mutually exclusive.`, }, + cli.StringFlag{ + Name: "attestation-ca-url", + Usage: "The base url of the Attestation CA to use", + }, + cli.StringFlag{ + Name: "attestation-ca-root", + Usage: "The path to the PEM with trusted roots when connecting to the Attestation CA", + }, + cli.BoolFlag{ + Name: "attestation-ca-insecure", + Usage: "Disables TLS server validation when connecting to the Attestation CA", + Hidden: true, + }, + cli.StringFlag{ + Name: "tpm-storage-directory", + Usage: "The directory where TPM keys and certificates will be stored", + Value: filepath.Join(step.Path(), "tpm"), + }, flags.TemplateSet, flags.TemplateSetFile, flags.CaConfig, @@ -171,7 +200,7 @@ multiple SANs. The '--san' flag and the '--token' flag are mutually exclusive.`, flags.Force, flags.Offline, flags.PasswordFile, - consoleFlag, + flags.Console, flags.KMSUri, flags.X5cCert, flags.X5cKey, @@ -206,10 +235,16 @@ func certificateAction(ctx *cli.Context) error { offline := ctx.Bool("offline") sans := ctx.StringSlice("san") - // offline and token are incompatible because the token is generated before - // the start of the offline CA. - if offline && tok != "" { + switch { + case offline && tok != "": + // offline and token are incompatible because the token is generated before + // the start of the offline CA. return errs.IncompatibleFlagWithFlag(ctx, "offline", "token") + case ctx.String("attestation-uri") != "" && ctx.String("kms") != "": + // attestation-uri and kms are incompatible because the ACME-DA flow + // expects all necessary parameters in the attestation-uri, and having + // both can be confusing. + return errs.IncompatibleFlagWithFlag(ctx, "attestation-uri", "kms") } // certificate flow unifies online and offline flows on a single api diff --git a/command/ca/federation.go b/command/ca/federation.go index af971e29..a1f3da74 100644 --- a/command/ca/federation.go +++ b/command/ca/federation.go @@ -6,16 +6,18 @@ import ( "os" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/pki" - "github.com/smallstep/cli/flags" - "github.com/smallstep/cli/utils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/fileutil" + "github.com/smallstep/cli-utils/ui" "go.step.sm/crypto/pemutil" + + "github.com/smallstep/cli/flags" ) type flowType int @@ -168,7 +170,7 @@ func rootsAndFederationFlow(ctx *cli.Context, typ flowType) error { } if outFile := ctx.Args().Get(0); outFile != "" { - if err := utils.WriteFile(outFile, data, 0600); err != nil { + if err := fileutil.WriteFile(outFile, data, 0o600); err != nil { return err } diff --git a/command/ca/health.go b/command/ca/health.go index 37fbea88..3ad013c5 100644 --- a/command/ca/health.go +++ b/command/ca/health.go @@ -1,14 +1,17 @@ package ca import ( + "context" "fmt" "os" + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) func healthCommand() cli.Command { @@ -73,11 +76,11 @@ func healthAction(ctx *cli.Context) error { var options []ca.ClientOption options = append(options, ca.WithRootFile(root)) - client, err := ca.NewClient(caURL, options...) + caClient, err := ca.NewClient(caURL, options...) if err != nil { return err } - r, err := client.Health() + r, err := caClient.HealthWithContext(context.Background()) if err != nil { return err } diff --git a/command/ca/health_test.go b/command/ca/health_test.go new file mode 100644 index 00000000..7b422b4b --- /dev/null +++ b/command/ca/health_test.go @@ -0,0 +1,99 @@ +package ca + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" + + "go.step.sm/crypto/minica" + "go.step.sm/crypto/pemutil" + + "github.com/smallstep/certificates/authority/config" + stepca "github.com/smallstep/certificates/ca" +) + +// reservePort "reserves" a TCP port by opening a listener on a random +// port and immediately closing it. The port can then be assumed to be +// available for running a server on. +func reservePort(t *testing.T) (host, port string) { + t.Helper() + l, err := net.Listen("tcp", ":0") + require.NoError(t, err) + + address := l.Addr().String() + err = l.Close() + require.NoError(t, err) + + host, port, err = net.SplitHostPort(address) + require.NoError(t, err) + + return +} + +func Test_healthAction(t *testing.T) { + dir := t.TempDir() + m, err := minica.New(minica.WithName("Step Integration")) + require.NoError(t, err) + + rootFilepath := filepath.Join(dir, "root.crt") + _, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath)) + require.NoError(t, err) + + intermediateCertFilepath := filepath.Join(dir, "intermediate.crt") + _, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath)) + require.NoError(t, err) + + intermediateKeyFilepath := filepath.Join(dir, "intermediate.key") + _, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath)) + require.NoError(t, err) + + // get a random address to listen on and connect to; currently no nicer way to get one before starting the server + // TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it? + host, port := reservePort(t) + + cfg := &config.Config{ + Root: []string{rootFilepath}, + IntermediateCert: intermediateCertFilepath, + IntermediateKey: intermediateKeyFilepath, + Address: net.JoinHostPort(host, port), // reuse the address that was just "reserved" + DNSNames: []string{"127.0.0.1", "[::1]", "localhost"}, + AuthorityConfig: &config.AuthConfig{ + AuthorityID: "stepca-test", + DeploymentType: "standalone-test", + }, + Logger: json.RawMessage(`{"format": "text"}`), + } + c, err := stepca.New(cfg) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + err = c.Run() + require.ErrorIs(t, err, http.ErrServerClosed) + }() + + caCommand := cli.Command{Name: "ca"} + caCommand.Subcommands = []cli.Command{healthCommand()} + + app := cli.NewApp() + app.Commands = cli.Commands{caCommand} + err = app.Run([]string{"step", "ca", "health", "--root", rootFilepath, "--ca-url", fmt.Sprintf("https://localhost:%s", port)}) + assert.NoError(t, err) + + // done testing; stop and wait for the server to quit + err = c.Stop() + require.NoError(t, err) + + wg.Wait() +} diff --git a/command/ca/init.go b/command/ca/init.go index ee1943a5..52871389 100644 --- a/command/ca/init.go +++ b/command/ca/init.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "crypto/x509" + stderrors "errors" "fmt" "io" "net" @@ -13,21 +14,20 @@ import ( "github.com/manifoldco/promptui" "github.com/pkg/errors" - "github.com/smallstep/certificates/cas/apiv1" - "github.com/smallstep/certificates/pki" - "github.com/smallstep/cli/flags" - "github.com/smallstep/cli/utils" - "github.com/smallstep/cli/utils/cautils" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/step" - "go.step.sm/cli-utils/ui" + "github.com/smallstep/certificates/cas/apiv1" + "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/step" + "github.com/smallstep/cli-utils/ui" "go.step.sm/crypto/kms" + _ "go.step.sm/crypto/kms/azurekms" // enable azurekms "go.step.sm/crypto/pemutil" - // Enable azurekms - _ "go.step.sm/crypto/kms/azurekms" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils" + "github.com/smallstep/cli/utils/cautils" ) func initCommand() cli.Command { @@ -36,12 +36,12 @@ func initCommand() cli.Command { Action: cli.ActionFunc(initAction), Usage: "initialize the CA PKI", UsageText: `**step ca init** -[**--root**=] [**--key**=] [**--pki**] [**--ssh**] +[**--root**=] [**--key**=] [**--key-password-file**=] [**--pki**] [**--ssh**] [**--helm**] [**--deployment-type**=] [**--name**=] -[**--dns**=] [**--address**=
] [**--provisioner**=] -[**--admin-subject**=] [**--provisioner-password-file**=] -[**--password-file**=] [**--ra**=] [**--kms**=] -[**--with-ca-url**=] [**--no-db**] [**--remote-management**] +[**--dns**=] [**--address**=
] [**--provisioner**=] +[**--admin-subject**=] [**--provisioner-password-file**=] +[**--password-file**=] [**--ra**=] [**--kms**=] +[**--with-ca-url**=] [**--no-db**] [**--remote-management**] [**--acme**] [**--context**=] [**--profile**=] [**--authority**=]`, Description: `**step ca init** command initializes a public key infrastructure (PKI) to be used by the Certificate Authority.`, @@ -56,6 +56,10 @@ func initCommand() cli.Command { Usage: "The path of an existing key of the root certificate authority.", EnvVar: step.IgnoreEnvVar, }, + cli.StringFlag{ + Name: "key-password-file", + Usage: `The path to the containing the password to decrypt the existing root certificate key.`, + }, cli.BoolFlag{ Name: "pki", Usage: "Generate only the PKI without the CA configuration.", @@ -239,10 +243,14 @@ func initAction(ctx *cli.Context) (err error) { case root == "" && key != "": return errs.RequiredWithFlag(ctx, "key", "root") case root != "" && key != "": + opts := []pemutil.Options{} + if keyPasswordFile := ctx.String("key-password-file"); keyPasswordFile != "" { + opts = append(opts, pemutil.WithPasswordFile(keyPasswordFile)) + } if rootCrt, err = pemutil.ReadCertificate(root); err != nil { return err } - if rootKey, err = pemutil.Read(key); err != nil { + if rootKey, err = pemutil.Read(key, opts...); err != nil { return err } case ra != "" && ra != apiv1.CloudCAS && ra != apiv1.StepCAS: @@ -267,7 +275,7 @@ func initAction(ctx *cli.Context) (err error) { case firstSuperAdminSubject != "" && !enableRemoteManagement: // providing the first super admin subject only works with DB-backed provisioners, // thus remote management should be enabled. - return errs.IncompatibleFlagWithFlag(ctx, "admin-subject", "remote-management") + return errors.New("flag '--admin-subject' is only supported when '--remote-management' is enabled") } var password string @@ -429,10 +437,10 @@ func initAction(ctx *cli.Context) (err error) { } if deploymentType == pki.HostedDeployment { ui.Println() - ui.Println("Sorry, we can't create hosted authorities from the CLI yet. To create a hosted") - ui.Println("authority please visit:\n") + ui.Println("To use a Hosted authority, you'll need a Smallstep account. To create one,") + ui.Println("visit:\n") ui.Println(" 033円[1mhttps://u.step.sm/hosted033円[0m\n") - ui.Println("To connect to an existing hosted authority run:\n") + ui.Println("Then, to connect to your hosted authority, run:\n") ui.Println(" $ step ca bootstrap --team --authority ") ui.Println() return nil @@ -467,7 +475,7 @@ func initAction(ctx *cli.Context) (err error) { if v, ok := keyManager.(interface{ ValidateName(s string) error }); ok { validateFunc = v.ValidateName } else { - validateFunc = func(s string) error { + validateFunc = func(_ string) error { return nil } } @@ -522,9 +530,9 @@ func initAction(ctx *cli.Context) (err error) { if pkiOnly { pkiOpts = append(pkiOpts, pki.WithPKIOnly()) } else { - ui.Println("What DNS names or IP addresses would you like to add to your new CA?", + ui.Println("What DNS names or IP addresses will clients use to reach your CA?", ui.WithSliceValue(ctx.StringSlice("dns"))) - dnsValue, err := ui.Prompt("(e.g. ca.smallstep.com[,1.1.1.1,etc.])", + dnsValue, err := ui.Prompt("(e.g. ca.example.com[,10.1.2.3,etc.])", ui.WithSliceValue(ctx.StringSlice("dns"))) if err != nil { return err @@ -565,7 +573,7 @@ func initAction(ctx *cli.Context) (err error) { if helm { ui.Println("What IP and port will your new CA bind to (it should match service.targetPort)?", ui.WithValue(ctx.String("address"))) } else { - ui.Println("What IP and port will your new CA bind to?", ui.WithValue(ctx.String("address"))) + ui.Println("What IP and port will your new CA bind to? (:443 will bind to 0.0.0.0:443)", ui.WithValue(ctx.String("address"))) } address, err = ui.Prompt("(e.g. :443 or 127.0.0.1:443)", ui.WithValidateFunc(ui.Address()), ui.WithValue(ctx.String("address"))) @@ -823,8 +831,11 @@ func processDNSValue(dnsValue string) ([]string, error) { ) dnsValue = strings.ReplaceAll(dnsValue, " ", ",") parts := strings.Split(dnsValue, ",") + if allEmpty(parts) { + return nil, stderrors.New("dns must not be empty") + } for _, name := range parts { - if name == "" { + if name == "" { // skip empty name continue } if err := dnsValidator(name); err != nil { @@ -845,3 +856,14 @@ func normalize(name string) string { } return name } + +// allEmpty loops through all strings in the slice and returns if +// all are empty (length 0). +func allEmpty(parts []string) bool { + for _, p := range parts { + if p != "" { + return false + } + } + return true +} diff --git a/command/ca/init_test.go b/command/ca/init_test.go index 0770623f..06aa7579 100644 --- a/command/ca/init_test.go +++ b/command/ca/init_test.go @@ -14,6 +14,19 @@ func Test_processDNSValue(t *testing.T) { want []string wantErr bool }{ + + { + name: "fail/empty", + dnsValue: "", + want: nil, + wantErr: true, + }, + { + name: "fail/empty-multiple", + dnsValue: ",,", + want: nil, + wantErr: true, + }, { name: "fail/dns", dnsValue: "ca.smallstep.com:8443", @@ -44,6 +57,12 @@ func Test_processDNSValue(t *testing.T) { want: []string{"ca.smallstep.com", "ca.localhost"}, wantErr: false, }, + { + name: "ok/multi-dns-with-skip", + dnsValue: "ca.smallstep.com,ca.localhost,,test.localhost", + want: []string{"ca.smallstep.com", "ca.localhost", "test.localhost"}, + wantErr: false, + }, { name: "ok/multi-space-dns", dnsValue: "ca.smallstep.com ca.localhost", diff --git a/command/ca/policy/actions/cn.go b/command/ca/policy/actions/cn.go index 731a8def..8c75efea 100644 --- a/command/ca/policy/actions/cn.go +++ b/command/ca/policy/actions/cn.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -55,7 +56,7 @@ $ step ca policy authority x509 deny cn "My Bad CA Name" commonNamesAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, flags.EABKeyID, flags.EABReference, cli.BoolFlag{ @@ -75,9 +76,12 @@ $ step ca policy authority x509 deny cn "My Bad CA Name" } func commonNamesAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -87,7 +91,7 @@ func commonNamesAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return fmt.Errorf("error retrieving policy: %w", err) } @@ -112,7 +116,7 @@ func commonNamesAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/dns.go b/command/ca/policy/actions/dns.go index b163b0fe..9fd30bf8 100644 --- a/command/ca/policy/actions/dns.go +++ b/command/ca/policy/actions/dns.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -73,7 +74,7 @@ $ step ca policy authority ssh host allow dns "badsshhost.local" dnsAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, flags.EABKeyID, flags.EABReference, cli.BoolFlag{ @@ -93,9 +94,12 @@ $ step ca policy authority ssh host allow dns "badsshhost.local" } func dnsAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -105,7 +109,7 @@ func dnsAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return fmt.Errorf("error retrieving policy: %w", err) } @@ -137,7 +141,7 @@ func dnsAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/emails.go b/command/ca/policy/actions/emails.go index e426e857..d92697b5 100644 --- a/command/ca/policy/actions/emails.go +++ b/command/ca/policy/actions/emails.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -62,7 +63,7 @@ $ step ca policy provisioner ssh user deny email @example.com --provisioner my_p emailAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, cli.BoolFlag{ Name: "remove", Usage: `removes the provided emails from the policy instead of adding them`, @@ -80,9 +81,12 @@ $ step ca policy provisioner ssh user deny email @example.com --provisioner my_p } func emailAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -92,7 +96,7 @@ func emailAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return err } @@ -124,7 +128,7 @@ func emailAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/ips.go b/command/ca/policy/actions/ips.go index e394b8ec..992861d7 100644 --- a/command/ca/policy/actions/ips.go +++ b/command/ca/policy/actions/ips.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -93,7 +94,7 @@ $ step ca policy authority ssh host deny ip 192.168.0.40 ipAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, flags.EABKeyID, flags.EABReference, cli.BoolFlag{ @@ -113,9 +114,12 @@ $ step ca policy authority ssh host deny ip 192.168.0.40 } func ipAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -125,7 +129,7 @@ func ipAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return err } @@ -157,7 +161,7 @@ func ipAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/policy.go b/command/ca/policy/actions/policy.go index 04b6cd55..82b77f95 100644 --- a/command/ca/policy/actions/policy.go +++ b/command/ca/policy/actions/policy.go @@ -7,33 +7,47 @@ import ( "errors" "fmt" - "github.com/urfave/cli" "google.golang.org/protobuf/encoding/protojson" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" - "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/internal/command" ) -var provisionerFilterFlag = cli.StringFlag{ - Name: "provisioner", - Usage: `The provisioner `, +func retrieveAndUnsetProvisionerFlagIfRequired(ctx context.Context) string { + // when managing policies on the authority level there's no need + // to select a provisioner, so the flag does not need to be unset. + if policycontext.IsAuthorityPolicyLevel(ctx) { + return "" + } + + clictx := command.CLIContextFromContext(ctx) + provisioner := clictx.String("provisioner") + + // unset the provisioner and issuer flag values, so that they're not used + // automatically in token flows. + if err := clictx.Set("provisioner", ""); err != nil { + panic(fmt.Errorf("failed unsetting provisioner flag: %w", err)) + } + if err := clictx.Set("issuer", ""); err != nil { + panic(fmt.Errorf("failed unsetting issuer flag: %w", err)) + } + + return provisioner } -func retrieveAndInitializePolicy(ctx context.Context, client *ca.AdminClient) (*linkedca.Policy, error) { +func retrieveAndInitializePolicy(ctx context.Context, client *ca.AdminClient, provisioner string) (*linkedca.Policy, error) { var ( - policy *linkedca.Policy - err error + clictx = command.CLIContextFromContext(ctx) + reference = clictx.String("eab-key-reference") + keyID = clictx.String("eab-key-id") + policy *linkedca.Policy + err error ) - clictx := command.CLIContextFromContext(ctx) - provisioner := clictx.String("provisioner") - reference := clictx.String("eab-key-reference") - keyID := clictx.String("eab-key-id") - switch { case policycontext.IsAuthorityPolicyLevel(ctx): policy, err = client.GetAuthorityPolicy() @@ -147,13 +161,11 @@ func initPolicy(p *linkedca.Policy) *linkedca.Policy { return p } -func updatePolicy(ctx context.Context, client *ca.AdminClient, policy *linkedca.Policy) (*linkedca.Policy, error) { - clictx := command.CLIContextFromContext(ctx) - provisioner := clictx.String("provisioner") - reference := clictx.String("eab-key-reference") - keyID := clictx.String("eab-key-id") - +func updatePolicy(ctx context.Context, client *ca.AdminClient, policy *linkedca.Policy, provisioner string) (*linkedca.Policy, error) { var ( + clictx = command.CLIContextFromContext(ctx) + reference = clictx.String("eab-key-reference") + keyID = clictx.String("eab-key-id") updatedPolicy *linkedca.Policy err error ) diff --git a/command/ca/policy/actions/principals.go b/command/ca/policy/actions/principals.go index 1c744bf1..2b845ada 100644 --- a/command/ca/policy/actions/principals.go +++ b/command/ca/policy/actions/principals.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -57,7 +58,7 @@ $ step ca policy provisioner ssh host deny principal root --provisioner my_ssh_u principalAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, cli.BoolFlag{ Name: "remove", Usage: `removes the provided Principals from the policy instead of adding them`, @@ -75,9 +76,12 @@ $ step ca policy provisioner ssh host deny principal root --provisioner my_ssh_u } func principalAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -87,7 +91,7 @@ func principalAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return err } @@ -119,7 +123,7 @@ func principalAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/remove.go b/command/ca/policy/actions/remove.go index 6cb7528b..7171329f 100644 --- a/command/ca/policy/actions/remove.go +++ b/command/ca/policy/actions/remove.go @@ -5,13 +5,15 @@ import ( "errors" "fmt" + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" "github.com/smallstep/cli/internal/command" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) // RemoveCommand returns the policy remove subcommand. @@ -53,7 +55,7 @@ $ step ca policy acme remove --provisioner my_acme_provisioner --eab-key-id "lUO removeAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, flags.EABKeyID, flags.EABReference, flags.AdminCert, @@ -69,10 +71,12 @@ $ step ca policy acme remove --provisioner my_acme_provisioner --eab-key-id "lUO } func removeAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) - provisioner := clictx.String("provisioner") - reference := clictx.String("eab-key-reference") - keyID := clictx.String("eab-key-id") + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + reference = clictx.String("eab-key-reference") + keyID = clictx.String("eab-key-id") + ) client, err := cautils.NewAdminClient(clictx) if err != nil { diff --git a/command/ca/policy/actions/uris.go b/command/ca/policy/actions/uris.go index 46e4ea97..203625e6 100644 --- a/command/ca/policy/actions/uris.go +++ b/command/ca/policy/actions/uris.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" @@ -52,7 +53,7 @@ $ step ca policy provisioner x509 allow uri "*.example.com" --provisioner my_pro uriAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, cli.BoolFlag{ Name: "remove", Usage: `removes the provided URIs from the policy instead of adding them`, @@ -70,9 +71,12 @@ $ step ca policy provisioner x509 allow uri "*.example.com" --provisioner my_pro } func uriAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + args = clictx.Args() + ) - args := clictx.Args() if len(args) == 0 { return errs.TooFewArguments(clictx) } @@ -82,7 +86,7 @@ func uriAction(ctx context.Context) (err error) { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return fmt.Errorf("error retrieving policy: %w", err) } @@ -107,7 +111,7 @@ func uriAction(ctx context.Context) (err error) { panic("no SSH nor X.509 context set") } - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/actions/view.go b/command/ca/policy/actions/view.go index 01f0c189..543dc36c 100644 --- a/command/ca/policy/actions/view.go +++ b/command/ca/policy/actions/view.go @@ -5,14 +5,16 @@ import ( "errors" "fmt" + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/command/ca/policy/policycontext" "github.com/smallstep/cli/flags" "github.com/smallstep/cli/internal/command" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) // ViewCommand returns the policy view subcommand @@ -54,7 +56,7 @@ $ step ca policy acme view --provisioner my_acme_provisioner --eab-key-id "lUOTG viewAction, ), Flags: []cli.Flag{ - provisionerFilterFlag, + flags.Provisioner, flags.EABKeyID, flags.EABReference, flags.AdminCert, @@ -70,20 +72,19 @@ $ step ca policy acme view --provisioner my_acme_provisioner --eab-key-id "lUOTG } func viewAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) - provisioner := clictx.String("provisioner") - reference := clictx.String("eab-key-reference") - keyID := clictx.String("eab-key-id") + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + reference = clictx.String("eab-key-reference") + keyID = clictx.String("eab-key-id") + policy *linkedca.Policy + ) client, err := cautils.NewAdminClient(clictx) if err != nil { return fmt.Errorf("error creating admin client: %w", err) } - var ( - policy *linkedca.Policy - ) - switch { case policycontext.IsAuthorityPolicyLevel(ctx): policy, err = client.GetAuthorityPolicy() diff --git a/command/ca/policy/actions/wildcards.go b/command/ca/policy/actions/wildcards.go index b80c9dd6..ec2483f8 100644 --- a/command/ca/policy/actions/wildcards.go +++ b/command/ca/policy/actions/wildcards.go @@ -10,21 +10,24 @@ import ( // AllowWildcardsAction updates the policy to allow wildcard names. func AllowWildcardsAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + ) client, err := cautils.NewAdminClient(clictx) if err != nil { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return fmt.Errorf("error retrieving policy: %w", err) } policy.X509.AllowWildcardNames = true - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } @@ -34,21 +37,24 @@ func AllowWildcardsAction(ctx context.Context) (err error) { // DenyWildcardsAction updates the policy to deny wildcard names. func DenyWildcardsAction(ctx context.Context) (err error) { - clictx := command.CLIContextFromContext(ctx) + var ( + provisioner = retrieveAndUnsetProvisionerFlagIfRequired(ctx) + clictx = command.CLIContextFromContext(ctx) + ) client, err := cautils.NewAdminClient(clictx) if err != nil { return fmt.Errorf("error creating admin client: %w", err) } - policy, err := retrieveAndInitializePolicy(ctx, client) + policy, err := retrieveAndInitializePolicy(ctx, client, provisioner) if err != nil { return fmt.Errorf("error retrieving policy: %w", err) } policy.X509.AllowWildcardNames = false - updatedPolicy, err := updatePolicy(ctx, client, policy) + updatedPolicy, err := updatePolicy(ctx, client, policy, provisioner) if err != nil { return fmt.Errorf("error updating policy: %w", err) } diff --git a/command/ca/policy/x509/allow.go b/command/ca/policy/x509/allow.go index d0e1c965..6b10d0d2 100644 --- a/command/ca/policy/x509/allow.go +++ b/command/ca/policy/x509/allow.go @@ -15,8 +15,8 @@ func allowCommand(ctx context.Context) cli.Command { return cli.Command{ Name: "allow", Usage: "manage allowed names for X.509 certificate issuance policies", - UsageText: "**step ca policy x509 allow** [arguments] [global-flags] [subcommand-flags]", - Description: `**step ca policy x509 allow** command group provides facilities for managing X.509 names to be allowed.`, + UsageText: "**step ca policy x509 allow** [arguments] [global-flags] [subcommand-flags]", + Description: `**step ca policy x509 allow** command group provides facilities for managing X.509 names to be allowed.`, Subcommands: cli.Commands{ actions.CommonNamesCommand(ctx), actions.DNSCommand(ctx), diff --git a/command/ca/policy/x509/deny.go b/command/ca/policy/x509/deny.go index d4190a10..b6742a2d 100644 --- a/command/ca/policy/x509/deny.go +++ b/command/ca/policy/x509/deny.go @@ -15,8 +15,8 @@ func denyCommand(ctx context.Context) cli.Command { return cli.Command{ Name: "deny", Usage: "manage denied names for X.509 certificate issuance policies", - UsageText: "**step ca policy x509 deny** [arguments] [global-flags] [subcommand-flags]", - Description: `**step ca policy x509 deny** command group provides facilities for managing X.509 names to be denied.`, + UsageText: "**step ca policy x509 deny** [arguments] [global-flags] [subcommand-flags]", + Description: `**step ca policy x509 deny** command group provides facilities for managing X.509 names to be denied.`, Subcommands: cli.Commands{ actions.CommonNamesCommand(ctx), actions.DNSCommand(ctx), diff --git a/command/ca/policy/x509/wildcards.go b/command/ca/policy/x509/wildcards.go index c52ca6bd..108d4945 100644 --- a/command/ca/policy/x509/wildcards.go +++ b/command/ca/policy/x509/wildcards.go @@ -18,8 +18,8 @@ func wildcardsCommand(ctx context.Context) cli.Command { return cli.Command{ Name: "wildcards", Usage: "manage wildcard name settings for X.509 certificate issuance policies", - UsageText: `**step ca policy x509 wildcards**`, - Description: `**step ca policy x509 wildcards** command group provides facilities for managing X.509 wildcard names.`, + UsageText: `**step ca policy x509 wildcards**`, + Description: `**step ca policy x509 wildcards** command group provides facilities for managing X.509 wildcard names.`, Subcommands: cli.Commands{ allowWildcardsCommand(ctx), denyWildcardsCommand(ctx), @@ -31,12 +31,12 @@ func allowWildcardsCommand(ctx context.Context) cli.Command { return cli.Command{ Name: "allow", Usage: "allow wildcard names in X.509 certificate issuance policies", - UsageText: `**step ca policy x509 wildcards allow** + UsageText: `**step ca policy x509 wildcards allow** [**--provisioner**=] [**--eab-key-id**=] [**--eab-key-reference**=] [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=]`, - Description: `**step ca policy x509 wildcards allow** allow wildcard names in X.509 policy + Description: `**step ca policy x509 wildcards allow** allow wildcard names in X.509 policy ## EXAMPLES @@ -78,12 +78,12 @@ func denyWildcardsCommand(ctx context.Context) cli.Command { return cli.Command{ Name: "deny", Usage: "deny wildcard names in X.509 certificate issuance policies", - UsageText: `**step ca policy x509 wildcards deny** + UsageText: `**step ca policy x509 wildcards deny** [**--provisioner**=] [**--eab-key-id**=] [**--eab-key-reference**=] [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=]`, - Description: `**step ca policy x509 wildcards deny** deny wildcard names in X.509 policy + Description: `**step ca policy x509 wildcards deny** deny wildcard names in X.509 policy ## EXAMPLES diff --git a/command/ca/policy/x509/x509.go b/command/ca/policy/x509/x509.go index 2dbafc9b..0bb0dbb3 100644 --- a/command/ca/policy/x509/x509.go +++ b/command/ca/policy/x509/x509.go @@ -14,8 +14,8 @@ func Command(ctx context.Context) cli.Command { return cli.Command{ Name: "x509", Usage: "manage X.509 certificate issuance policies", - UsageText: `**step ca policy x509** [arguments] [global-flags] [subcommand-flags]`, - Description: `**step ca policy x509** command group provides facilities for managing X.509 certificate issuance policies.`, + UsageText: `**step ca policy x509** [arguments] [global-flags] [subcommand-flags]`, + Description: `**step ca policy x509** command group provides facilities for managing X.509 certificate issuance policies.`, Subcommands: cli.Commands{ allowCommand(ctx), denyCommand(ctx), diff --git a/command/ca/provisioner/add.go b/command/ca/provisioner/add.go index 8d94d1de..a6b081a7 100644 --- a/command/ca/provisioner/add.go +++ b/command/ca/provisioner/add.go @@ -1,29 +1,34 @@ package provisioner import ( + "bytes" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/x509" "encoding/pem" + "fmt" "net/url" "os" "strings" "github.com/pkg/errors" - "github.com/smallstep/cli/flags" - "github.com/smallstep/cli/internal/sliceutil" - "github.com/smallstep/cli/utils" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" + + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + "github.com/smallstep/linkedca" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" - "go.step.sm/linkedca" + + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cast" + "github.com/smallstep/cli/internal/sliceutil" + "github.com/smallstep/cli/utils" ) func addCommand() cli.Command { - return cli.Command{ + return cli.Command{ // #nosec G101 -- Google OIDC example values Name: "add", Action: cli.ActionFunc(addAction), Usage: "add a provisioner", @@ -32,6 +37,8 @@ func addCommand() cli.Command { [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] ACME @@ -41,6 +48,7 @@ ACME [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] OIDC @@ -51,6 +59,8 @@ OIDC [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] X5C @@ -58,6 +68,8 @@ X5C [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] SSHPOP @@ -73,33 +85,41 @@ Nebula [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] -K8SSA +K8SSA (Kubernetes Service Account) **step ca provisioner add** **--type**=K8SSA [**--public-key**=] [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] -IID +IID (AWS/GCP/Azure) **step ca provisioner add** **--type**=[AWS|Azure|GCP] -[**--aws-account**=] [**--gcp-service-account**=] [**--gcp-project**=] +[**--aws-account**=] +[**--gcp-service-account**=] [**--gcp-project**=] [**--gcp-organization**=] [**--azure-tenant**=] [**--azure-resource-group**=] [**--azure-audience**=] [**--azure-subscription-id**=] [**--azure-object-id**=] [**--instance-age**=] [**--iid-roots**=] [**--disable-custom-sans**] [**--disable-trust-on-first-use**] +[**--disable-ssh-ca-user**] [**--disable-ssh-ca-host**] [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] SCEP **step ca provisioner add** **--type**=SCEP [**--force-cn**] [**--challenge**=] -[**--capabilities**=] [**--include-root**] [**--min-public-key-length**=] -[**--encryption-algorithm-identifier**=] -[**--admin-cert**=] [**--admin-key**=] -[**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] -[**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=]`, +[**--capabilities**=] [**--include-root**] [**--exclude-intermediate**] +[**--min-public-key-length**=] [**--encryption-algorithm-identifier**=] +[**--scep-decrypter-certificate-file**=] [**--scep-decrypter-key-file**=] +[**--scep-decrypter-key-uri**=] [**--scep-decrypter-key-password-file**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=]`, Flags: []cli.Flag{ // General provisioner flags typeFlag, @@ -135,8 +155,13 @@ SCEP // SCEP provisioner flags scepCapabilitiesFlag, scepIncludeRootFlag, + scepExcludeIntermediateFlag, scepMinimumPublicKeyLengthFlag, scepEncryptionAlgorithmIdentifierFlag, + scepDecrypterCertFileFlag, + scepDecrypterKeyFileFlag, + scepDecrypterKeyURIFlag, + scepDecrypterKeyPasswordFileFlag, // Cloud provisioner flags awsAccountFlag, @@ -147,9 +172,12 @@ SCEP azureObjectIDFlag, gcpServiceAccountFlag, gcpProjectFlag, + gcpOrganizationFlag, instanceAgeFlag, disableCustomSANsFlag, disableTOFUFlag, + disableSSHCAUserFlag, + disableSSHCAHostFlag, // Claims x509TemplateFlag, @@ -167,6 +195,7 @@ SCEP sshHostDefaultDurFlag, disableRenewalFlag, allowRenewalAfterExpiryFlag, + disableSmallstepExtensionsFlag, //enableX509Flag, enableSSHFlag, @@ -262,12 +291,17 @@ $ step ca provisioner add Azure --type Azure \ --azure-object-id f50926c7-abbf-4c28-87dc-9adc7eaf3ba7 ''' -Create an GCP provisioner that will only accept the SANs provided in the identity token: +Create a GCP provisioner that will only accept the SANs provided in the identity token: ''' $ step ca provisioner add Google --type GCP \ --disable-custom-sans --gcp-project internal ''' +Create a GCP provisioner that can be used across all projects within an organization: +''' +$ step ca provisioner add Google --type GCP --gcp-organization 123456789 +''' + Create an AWS provisioner that will only accept the SANs provided in the identity document and will allow multiple certificates from the same instance: ''' @@ -358,10 +392,11 @@ func addAction(ctx *cli.Context) (err error) { Ssh: &linkedca.SSHClaims{ UserDurations: &linkedca.Durations{}, HostDurations: &linkedca.Durations{}, - Enabled: !(ctx.IsSet("ssh") && !ctx.Bool("ssh")), + Enabled: !(ctx.IsSet("ssh") && !ctx.Bool("ssh")), //nolint:staticcheck // TODO(hs): fix this }, - DisableRenewal: ctx.Bool("disable-renewal"), - AllowRenewalAfterExpiry: ctx.Bool("allow-renewal-after-expiry"), + DisableRenewal: ctx.Bool("disable-renewal"), + AllowRenewalAfterExpiry: ctx.Bool("allow-renewal-after-expiry"), + DisableSmallstepExtensions: ctx.Bool("disable-smallstep-extensions"), } if ctx.IsSet("x509-min-dur") { @@ -438,7 +473,7 @@ func createJWKDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { password string ) - if passwordFile := ctx.String("password-file"); len(passwordFile)> 0 { + if passwordFile := ctx.String("password-file"); passwordFile != "" { password, err = utils.ReadStringPasswordFromFile(passwordFile) if err != nil { return nil, err @@ -578,7 +613,7 @@ func createACMEDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { }, nil } -func createSSHPOPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { +func createSSHPOPDetails(*cli.Context) (*linkedca.ProvisionerDetails, error) { return &linkedca.ProvisionerDetails{ Data: &linkedca.ProvisionerDetails_SSHPOP{ SSHPOP: &linkedca.SSHPOPProvisioner{}, @@ -720,6 +755,13 @@ func createOIDCDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { } func createAWSDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { + if ctx.IsSet("disable-ssh-ca-user") { + return nil, errors.New("flag disable-ssh-ca-user is not supported for AWS IID provisioners") + } + if ctx.IsSet("disable-ssh-ca-host") { + return nil, errors.New("flag disable-ssh-ca-host is not supported for AWS IID provisioners") + } + d, err := parseInstanceAge(ctx) if err != nil { return nil, err @@ -740,6 +782,13 @@ func createAWSDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { } func createAzureDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { + if ctx.IsSet("disable-ssh-ca-user") { + return nil, errors.New("flag disable-ssh-ca-user is not supported for Azure IID provisioners") + } + if ctx.IsSet("disable-ssh-ca-host") { + return nil, errors.New("flag disable-ssh-ca-host is not supported for Azure IID provisioners") + } + tenantID := ctx.String("azure-tenant") if tenantID == "" { return nil, errs.RequiredWithFlagValue(ctx, "type", ctx.String("type"), "azure-tenant") @@ -761,18 +810,39 @@ func createAzureDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) } func createGCPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { + if ctx.String("gcp-organization") != "" && len(ctx.StringSlice("gcp-project"))> 0 { + return nil, errs.IncompatibleFlagWithFlag(ctx, "gcp-organization", "gcp-project") + } + d, err := parseInstanceAge(ctx) if err != nil { return nil, err } + var ( + disableSSHCAUser *bool + disableSSHCAHost *bool + ) + + if ctx.IsSet("disable-ssh-ca-user") { + boolVal := ctx.Bool("disable-ssh-ca-user") + disableSSHCAUser = &boolVal + } + if ctx.IsSet("disable-ssh-ca-host") { + boolVal := ctx.Bool("disable-ssh-ca-host") + disableSSHCAHost = &boolVal + } + return &linkedca.ProvisionerDetails{ Data: &linkedca.ProvisionerDetails_GCP{ GCP: &linkedca.GCPProvisioner{ ServiceAccounts: ctx.StringSlice("gcp-service-account"), ProjectIds: ctx.StringSlice("gcp-project"), + OrganizationId: ctx.String("gcp-organization"), DisableCustomSans: ctx.Bool("disable-custom-sans"), DisableTrustOnFirstUse: ctx.Bool("disable-trust-on-first-use"), + DisableSshCaUser: disableSSHCAUser, + DisableSshCaHost: disableSSHCAHost, InstanceAge: d, }, }, @@ -785,16 +855,47 @@ func createSCEPDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) { if v := ctx.StringSlice("challenge"); len(v)> 0 { challenge = v[0] } + s := &linkedca.SCEPProvisioner{ + ForceCn: ctx.Bool("force-cn"), + Challenge: challenge, + Capabilities: ctx.StringSlice("capabilities"), + MinimumPublicKeyLength: cast.Int32(ctx.Int("min-public-key-length")), + IncludeRoot: ctx.Bool("include-root"), + ExcludeIntermediate: ctx.Bool("exclude-intermediate"), + EncryptionAlgorithmIdentifier: cast.Int32(ctx.Int("encryption-algorithm-identifier")), + } + decrypter := &linkedca.SCEPDecrypter{} + if decrypterCertificateFile := ctx.String("scep-decrypter-certificate-file"); decrypterCertificateFile != "" { + data, err := parseSCEPDecrypterCertificate(decrypterCertificateFile) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate from %q: %w", decrypterCertificateFile, err) + } + decrypter.Certificate = data + s.Decrypter = decrypter + } + if decrypterKeyURI := ctx.String("scep-decrypter-key-uri"); decrypterKeyURI != "" { + decrypter.KeyUri = decrypterKeyURI + s.Decrypter = decrypter + } + if decrypterKeyFile := ctx.String("scep-decrypter-key-file"); decrypterKeyFile != "" { + data, err := readSCEPDecrypterKey(decrypterKeyFile) + if err != nil { + return nil, fmt.Errorf("failed reading decrypter key from %q: %w", decrypterKeyFile, err) + } + decrypter.Key = data + s.Decrypter = decrypter + } + if decrypterKeyPasswordFile := ctx.String("scep-decrypter-key-password-file"); decrypterKeyPasswordFile != "" { + decrypterKeyPassword, err := utils.ReadPasswordFromFile(decrypterKeyPasswordFile) + if err != nil { + return nil, fmt.Errorf("failed reading decrypter key password from %q: %w", decrypterKeyPasswordFile, err) + } + decrypter.KeyPassword = decrypterKeyPassword + s.Decrypter = decrypter + } return &linkedca.ProvisionerDetails{ Data: &linkedca.ProvisionerDetails_SCEP{ - SCEP: &linkedca.SCEPProvisioner{ - ForceCn: ctx.Bool("force-cn"), - Challenge: challenge, - Capabilities: ctx.StringSlice("capabilities"), - MinimumPublicKeyLength: int32(ctx.Int("min-public-key-length")), - IncludeRoot: ctx.Bool("include-root"), - EncryptionAlgorithmIdentifier: int32(ctx.Int("encryption-algorithm-identifier")), - }, + SCEP: s, }, }, nil } @@ -903,3 +1004,48 @@ func parseCACertificates(filenames []string) ([][]byte, error) { } return pemCerts, nil } + +func parseSCEPDecrypterCertificate(filename string) ([]byte, error) { + certs, err := pemutil.ReadCertificateBundle(filename) + if err != nil { + return nil, fmt.Errorf("failed reading certificate from %q: %w", filename, err) + } + if len(certs) == 0 { + return nil, fmt.Errorf("no certificates found in %q", filename) + } + // TODO(hs): implement validation, such as key usage? + buf := bytes.Buffer{} + if err = pem.Encode(&buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certs[0].Raw, // assumes the bundle is a certificate chain; using first cert as decrypter + }); err != nil { + return nil, fmt.Errorf("failed encoding certificate: %w", err) + } + return buf.Bytes(), nil +} + +func readSCEPDecrypterKey(filename string) ([]byte, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed reading %q: %w", filename, err) + } + + if err := validateSCEPDecrypterKey(b); err != nil { + return nil, fmt.Errorf("failed decoding %q: %w", filename, err) + } + + // TODO(hs): additional validation that this is an (encrypted) private key? + + return b, err +} + +func validateSCEPDecrypterKey(data []byte) error { + block, rest := pem.Decode(data) + switch { + case block == nil: + return errors.New("not a valid PEM encoded block") + case len(bytes.TrimSpace(rest))> 0: + return errors.New("contains more than one PEM encoded block") + } + return nil +} diff --git a/command/ca/provisioner/caConfigClient.go b/command/ca/provisioner/caConfigClient.go index 5d993545..eceb20a6 100644 --- a/command/ca/provisioner/caConfigClient.go +++ b/command/ca/provisioner/caConfigClient.go @@ -4,12 +4,13 @@ import ( "context" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" - "go.step.sm/cli-utils/ui" - "go.step.sm/linkedca" + "github.com/smallstep/cli-utils/ui" + "github.com/smallstep/linkedca" ) // nodb implements the certificates/Adminclient interface with noops. @@ -19,62 +20,62 @@ func newNoDB() *nodb { return &nodb{} } -func (n *nodb) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { +func (n *nodb) CreateProvisioner(context.Context, *linkedca.Provisioner) error { return nil } -func (n *nodb) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) { +func (n *nodb) GetProvisioner(context.Context, string) (*linkedca.Provisioner, error) { //nolint:nilnil // nodb is a noop interface. return nil, nil } -func (n *nodb) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) { +func (n *nodb) GetProvisioners(context.Context) ([]*linkedca.Provisioner, error) { return nil, nil } -func (n *nodb) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { +func (n *nodb) UpdateProvisioner(context.Context, *linkedca.Provisioner) error { return nil } -func (n *nodb) DeleteProvisioner(ctx context.Context, id string) error { +func (n *nodb) DeleteProvisioner(context.Context, string) error { return nil } -func (n *nodb) CreateAdmin(ctx context.Context, admin *linkedca.Admin) error { +func (n *nodb) CreateAdmin(context.Context, *linkedca.Admin) error { return nil } -func (n *nodb) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) { +func (n *nodb) GetAdmin(context.Context, string) (*linkedca.Admin, error) { //nolint:nilnil // nodb is a noop interface. return nil, nil } -func (n *nodb) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) { +func (n *nodb) GetAdmins(context.Context) ([]*linkedca.Admin, error) { return nil, nil } -func (n *nodb) UpdateAdmin(ctx context.Context, prov *linkedca.Admin) error { +func (n *nodb) UpdateAdmin(context.Context, *linkedca.Admin) error { return nil } -func (n *nodb) DeleteAdmin(ctx context.Context, id string) error { +func (n *nodb) DeleteAdmin(context.Context, string) error { return nil } -func (n *nodb) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { +func (n *nodb) CreateAuthorityPolicy(context.Context, *linkedca.Policy) error { return nil } -func (n *nodb) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) { +func (n *nodb) GetAuthorityPolicy(context.Context) (*linkedca.Policy, error) { //nolint:nilnil // nodb is a noop interface. return nil, nil } -func (n *nodb) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { +func (n *nodb) UpdateAuthorityPolicy(context.Context, *linkedca.Policy) error { return nil } -func (n *nodb) DeleteAuthorityPolicy(ctx context.Context) error { +func (n *nodb) DeleteAuthorityPolicy(context.Context) error { return nil } @@ -92,8 +93,7 @@ func newCaConfigClient(ctx context.Context, cfg *config.Config, cfgFile string) } } a, err := authority.New(cfg, authority.WithAdminDB(newNoDB()), - //nolint:staticcheck // TODO: WithProvisioners has been deprecated, temporarily do not lint this line. - authority.WithSkipInit(), authority.WithProvisioners(provClxn)) + authority.WithSkipInit(), authority.WithProvisioners(provClxn)) //nolint:staticcheck // TODO: WithProvisioners has been deprecated, temporarily do not lint this line. if err != nil { return nil, errors.Wrapf(err, "error loading authority") } @@ -130,16 +130,15 @@ func (client *caConfigClient) GetProvisioner(opts ...ca.ProvisionerOption) (*lin return linkedcaProv, nil } +// NOTE: 'name' parameter has been deprecated and will be removed in a future +// minor release. func (client *caConfigClient) UpdateProvisioner(name string, prov *linkedca.Provisioner) error { + _ = name if err := client.auth.UpdateProvisioner(client.ctx, prov); err != nil { return errors.Wrapf(err, "error updating provisioner") } - if err := client.write(); err != nil { - return err - } - - return nil + return client.write() } func (client *caConfigClient) RemoveProvisioner(opts ...ca.ProvisionerOption) error { @@ -151,11 +150,7 @@ func (client *caConfigClient) RemoveProvisioner(opts ...ca.ProvisionerOption) er return errors.Wrapf(err, "error removing provisioner") } - if err := client.write(); err != nil { - return err - } - - return nil + return client.write() } func (client *caConfigClient) loadProvisioner(opts ...ca.ProvisionerOption) (provisioner.Interface, error) { diff --git a/command/ca/provisioner/getEncryptedKey.go b/command/ca/provisioner/getEncryptedKey.go index 00c61df6..1c9a78b8 100644 --- a/command/ca/provisioner/getEncryptedKey.go +++ b/command/ca/provisioner/getEncryptedKey.go @@ -4,10 +4,12 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) func getEncryptedKeyCommand() cli.Command { diff --git a/command/ca/provisioner/list.go b/command/ca/provisioner/list.go index cebd0bc1..4e5efeec 100644 --- a/command/ca/provisioner/list.go +++ b/command/ca/provisioner/list.go @@ -5,10 +5,12 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli/flags" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) func listCommand() cli.Command { diff --git a/command/ca/provisioner/provisioner.go b/command/ca/provisioner/provisioner.go index 8a0eba09..c0e9bcff 100644 --- a/command/ca/provisioner/provisioner.go +++ b/command/ca/provisioner/provisioner.go @@ -3,21 +3,23 @@ package provisioner import ( "context" "fmt" - "os" + "net" "time" "github.com/pkg/errors" nebula "github.com/slackhq/nebula/cert" + "github.com/urfave/cli" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/command/ca/provisioner/webhook" "github.com/smallstep/cli/utils" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/linkedca" ) // Command returns the jwk subcommand. @@ -101,38 +103,31 @@ type crudClient interface { } func newCRUDClient(cliCtx *cli.Context, cfgFile string) (crudClient, error) { - // os.Stat("") probably returns os.ErrNotExist, but this behavior is - // undocumented so we'll handle this case separately. - if cfgFile == "" { - return cautils.NewAdminClient(cliCtx) + unauthAdminClient, err := cautils.NewUnauthenticatedAdminClient(cliCtx) + if err != nil { + return nil, fmt.Errorf("error generating admin client: %w", err) } - _, err := os.Stat(cfgFile) + var netErr *net.OpError + + err = unauthAdminClient.IsEnabled() switch { - case errors.Is(err, os.ErrNotExist): - return cautils.NewAdminClient(cliCtx) - case err == nil: + case errors.As(err, &netErr) || errors.Is(err, ca.ErrAdminAPINotImplemented): ui.PrintSelected("CA Configuration", cfgFile) cfg, err := config.LoadConfiguration(cfgFile) if err != nil { return nil, fmt.Errorf("error loading configuration: %w", err) } - - if cfg.AuthorityConfig.EnableAdmin { - if len(cfg.AuthorityConfig.Provisioners)> 0 { - return nil, errors.New("when 'enableAdmin' attribute set to 'true', provisioners list in ca.json must be empty") - } - return cautils.NewAdminClient(cliCtx) - } - // Assume the ca.json is already valid to avoid enabling all the // features present in step-ca just to modify the provisioners. cfg.SkipValidation = true ui.Println() return newCaConfigClient(context.Background(), cfg, cfgFile) + case errors.Is(err, ca.ErrAdminAPINotAuthorized): + return cautils.NewAdminClient(cliCtx) default: - return nil, errs.FileError(err, cfgFile) + return nil, err } } @@ -255,6 +250,10 @@ unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", Name: "allow-renewal-after-expiry", Usage: `Allow renewals for expired certificates generated by this provisioner.`, } + disableSmallstepExtensionsFlag = cli.BoolFlag{ + Name: "disable-smallstep-extensions", + Usage: `Disable the Smallstep extension for all certificates generated by this provisioner.`, + } //enableX509Flag = cli.BoolFlag{ // Name: "x509", // Usage: `Enable provisioning of x509 certificates.`, @@ -411,6 +410,10 @@ Use the flag multiple times to remove multiple formats.`, Name: "include-root", Usage: `Include the CA root certificate in the SCEP CA certificate chain`, } + scepExcludeIntermediateFlag = cli.BoolFlag{ + Name: "exclude-intermediate", + Usage: `Exclude the CA intermediate certificate in the SCEP CA certificate chain`, + } scepMinimumPublicKeyLengthFlag = cli.IntFlag{ Name: "min-public-key-length", Usage: `The minimum public key of the SCEP RSA encryption key`, @@ -427,6 +430,23 @@ Use the flag multiple times to remove multiple formats.`, Defaults to DES-CBC (0) for legacy clients.`, } + scepDecrypterCertFileFlag = cli.StringFlag{ + Name: "scep-decrypter-certificate-file", + Usage: `The path to a PEM certificate for the SCEP decrypter`, + } + scepDecrypterKeyFileFlag = cli.StringFlag{ + Name: "scep-decrypter-key-file", + Usage: `The path to a PEM private key for the SCEP decrypter`, + } + scepDecrypterKeyURIFlag = cli.StringFlag{ + Name: "scep-decrypter-key-uri", + Usage: `The key for the SCEP decrypter. Should be a valid value for the KMS type used.`, + } + scepDecrypterKeyPasswordFileFlag = cli.StringFlag{ + Name: "scep-decrypter-key-password-file", + Usage: `The path to a containing the password for the SCEP decrypter key`, + } + // Cloud provisioner flags awsAccountFlag = cli.StringSliceFlag{ Name: "aws-account", @@ -496,6 +516,10 @@ Use the flag multiple times to configure multiple projects`, Usage: `Remove a Google project used to validate the identity tokens. Use the flag multiple times to remove multiple projects`, } + gcpOrganizationFlag = cli.StringFlag{ + Name: "gcp-organization", + Usage: `The Google organization used to validate the project in the identity tokens.`, + } instanceAgeFlag = cli.DurationFlag{ Name: "instance-age", Usage: `The maximum to grant a certificate in AWS and GCP provisioners. @@ -522,6 +546,16 @@ with the same instance will be accepted. By default only the first request will be accepted.`, } + disableSSHCAUserFlag = cli.BoolFlag{ + Name: "disable-ssh-ca-user", + Usage: `Disable ability to sign SSH user certificates`, + } + + disableSSHCAHostFlag = cli.BoolFlag{ + Name: "disable-ssh-ca-host", + Usage: `Disable ability to sign SSH host certificates`, + } + // Nebula provisioner flags nebulaRootFlag = cli.StringFlag{ Name: "nebula-root", @@ -585,7 +619,22 @@ Use the '--group' flag multiple times to configure multiple groups.`, } oidcTenantIDFlag = cli.StringFlag{ Name: "tenant-id", - Usage: `The used to replace the templatized {tenantid} in the OpenID Configuration.`, + Usage: `The used to replace the templatized tenantid value in the OpenID Configuration.`, + } + oidcScopeFlag = cli.StringSliceFlag{ + Name: "scope", + Usage: `The list used to validate the scopes extension in an OpenID Connect token. +Use the '--scope' flag multiple times to configure multiple scopes.`, + } + oidcRemoveScopeFlag = cli.StringSliceFlag{ + Name: "remove-scope", + Usage: `Remove the used to validate the scopes extension in an OpenID Connect token. +Use the '--remove-scope' flag multiple times to remove multiple scopes.`, + } + oidcAuthParamFlag = cli.StringSliceFlag{ + Name: "auth-param", + Usage: `The list used to validate the auth-params extension in an OpenID Connect token. +Use the '--auth-param' flag multiple times to configure multiple auth-params.`, } // X5C provisioner flags @@ -602,14 +651,14 @@ func readNebulaRoots(rootFile string) ([][]byte, error) { return nil, err } - var crt *nebula.NebulaCertificate - var certs []*nebula.NebulaCertificate + var crt nebula.Certificate + var certs []nebula.Certificate for len(b)> 0 { - crt, b, err = nebula.UnmarshalNebulaCertificateFromPEM(b) + crt, b, err = nebula.UnmarshalCertificateFromPEM(b) if err != nil { return nil, errors.Wrapf(err, "error reading %s", rootFile) } - if crt.Details.IsCA { + if crt.IsCA() { certs = append(certs, crt) } } @@ -619,7 +668,7 @@ func readNebulaRoots(rootFile string) ([][]byte, error) { rootBytes := make([][]byte, len(certs)) for i, crt := range certs { - b, err = crt.MarshalToPEM() + b, err = crt.MarshalPEM() if err != nil { return nil, errors.Wrap(err, "error marshaling certificate") } diff --git a/command/ca/provisioner/provisioner_test.go b/command/ca/provisioner/provisioner_test.go new file mode 100644 index 00000000..fe0168bf --- /dev/null +++ b/command/ca/provisioner/provisioner_test.go @@ -0,0 +1,97 @@ +package provisioner + +import ( + "crypto/ed25519" + "crypto/rand" + "net/netip" + "os" + "testing" + "time" + + nebula "github.com/slackhq/nebula/cert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadNebulaRoots(t *testing.T) { + t.Run("ok", func(t *testing.T) { + tempDir := t.TempDir() + ca, _ := mustNebulaCurve25519CA(t) + file, _ := serializeAndWriteNebulaCert(t, tempDir, ca) + + roots, err := readNebulaRoots(file) + assert.NoError(t, err) + assert.Len(t, roots, 1) + }) + + t.Run("fail/reading", func(t *testing.T) { + roots, err := readNebulaRoots("non-existing-file") + assert.Error(t, err) + assert.Empty(t, roots) + }) + + t.Run("fail/invalid-pem", func(t *testing.T) { + tempDir := t.TempDir() + + file, err := os.CreateTemp(tempDir, "nebula-test-cert-*") + require.NoError(t, err) + defer file.Close() + + _, err = file.Write([]byte{0}) + require.NoError(t, err) + + roots, err := readNebulaRoots(file.Name()) + assert.Error(t, err) + assert.Empty(t, roots) + }) + + t.Run("fail/no-certificates", func(t *testing.T) { + tempDir := t.TempDir() + + file, err := os.CreateTemp(tempDir, "nebula-test-cert-*") + require.NoError(t, err) + defer file.Close() + + roots, err := readNebulaRoots(file.Name()) + assert.Error(t, err) + assert.Empty(t, roots) + }) +} + +func mustNebulaCurve25519CA(t *testing.T) (nebula.Certificate, ed25519.PrivateKey) { + t.Helper() + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + tbs := &nebula.TBSCertificate{ + Version: nebula.Version1, + Name: "TestCA", + Groups: []string{"test"}, + Networks: []netip.Prefix{netip.MustParsePrefix("10.1.0.0/16")}, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(10 * time.Minute), + PublicKey: pub, + IsCA: true, + Curve: nebula.Curve_CURVE25519, + } + nc, err := tbs.Sign(nil, nebula.Curve_CURVE25519, priv) + require.NoError(t, err) + + return nc, priv +} + +func serializeAndWriteNebulaCert(t *testing.T, tempDir string, cert nebula.Certificate) (string, []byte) { + file, err := os.CreateTemp(tempDir, "nebula-test-cert-*") + require.NoError(t, err) + defer file.Close() + + pem, err := cert.MarshalPEM() + require.NoError(t, err) + data, err := cert.Marshal() + require.NoError(t, err) + _, err = file.Write(pem) + require.NoError(t, err) + + return file.Name(), data +} diff --git a/command/ca/provisioner/remove.go b/command/ca/provisioner/remove.go index 3fd6c59d..f4d0855e 100644 --- a/command/ca/provisioner/remove.go +++ b/command/ca/provisioner/remove.go @@ -1,10 +1,11 @@ package provisioner import ( + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" "github.com/smallstep/cli/flags" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" ) func removeCommand() cli.Command { diff --git a/command/ca/provisioner/update.go b/command/ca/provisioner/update.go index e3fe7018..98f6cabe 100644 --- a/command/ca/provisioner/update.go +++ b/command/ca/provisioner/update.go @@ -11,16 +11,20 @@ import ( "os" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + + "github.com/smallstep/linkedca" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/pemutil" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cast" "github.com/smallstep/cli/internal/sliceutil" "github.com/smallstep/cli/utils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/pemutil" - "go.step.sm/linkedca" ) func updateCommand() cli.Command { @@ -33,6 +37,8 @@ func updateCommand() cli.Command { [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] ACME @@ -42,6 +48,7 @@ ACME [**--attestation-roots**=] [**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] OIDC @@ -51,23 +58,30 @@ OIDC [**--domain**=] [**--remove-domain**=] [**--group**=] [**--remove-group**=] [**--admin**=]... [**--remove-admin**=]... -[**--admin-cert**=] [**--admin-key**=] +[**--scope**=] [**--remove-scope**=] +[**--auth-param**=] [**--remove-auth-param**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] X5C **step ca provisioner update** **--x5c-roots**= -[**--admin-cert**=] [**--admin-key**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] K8SSA (Kubernetes Service Account) **step ca provisioner update** [**--public-key**=] -[**--admin-cert**=] [**--admin-key**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] IID (AWS/GCP/Azure) @@ -75,21 +89,29 @@ IID (AWS/GCP/Azure) [**--aws-account**=]... [**--remove-aws-account**=]... [**--gcp-service-account**=]... [**--remove-gcp-service-account**=]... [**--gcp-project**=]... [**--remove-gcp-project**=]... +[**--gcp-organization**=] [**--azure-tenant**=] [**--azure-resource-group**=] [**--azure-audience**=] [**--azure-subscription-id**=] [**--azure-object-id**=] [**--instance-age**=] [**--disable-custom-sans**] [**--disable-trust-on-first-use**] -[**--admin-cert**=] [**--admin-key**=] +[**--disable-ssh-ca-user**] [**--disable-ssh-ca-host**] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] [**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=] [**--ssh-template**=] +[**--ssh-template-data**=] SCEP **step ca provisioner update** [**--force-cn**] [**--challenge**=] -[**--capabilities**=] [**--include-root**] [**--minimum-public-key-length**=] -[**--encryption-algorithm-identifier**=][**--admin-cert**=] [**--admin-key**=] -[**--admin-subject**=] [**--admin-provisioner**=] [**--admin-password-file**=] -[**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=]`, +[**--capabilities**=] [**--include-root**] [**--exclude-intermediate**] +[**--minimum-public-key-length**=] [**--encryption-algorithm-identifier**=] +[**--scep-decrypter-certificate-file**=] [**--scep-decrypter-key-file**=] +[**--scep-decrypter-key-uri**=] [**--scep-decrypter-key-password-file**=] +[**--admin-cert**=] [**--admin-key**=] [**--admin-subject**=] +[**--admin-provisioner**=] [**--admin-password-file**=] +[**--ca-url**=] [**--root**=] [**--context**=] [**--ca-config**=] +[**--x509-template**=] [**--x509-template-data**=]`, Flags: []cli.Flag{ nameFlag, pubKeyFlag, @@ -109,6 +131,9 @@ SCEP oidcRemoveDomainFlag, oidcGroupFlag, oidcTenantIDFlag, + oidcScopeFlag, + oidcRemoveScopeFlag, + oidcAuthParamFlag, // X5C Root Flag x5cRootsFlag, @@ -128,8 +153,13 @@ SCEP // SCEP flags scepCapabilitiesFlag, scepIncludeRootFlag, + scepExcludeIntermediateFlag, scepMinimumPublicKeyLengthFlag, scepEncryptionAlgorithmIdentifierFlag, + scepDecrypterCertFileFlag, + scepDecrypterKeyFileFlag, + scepDecrypterKeyURIFlag, + scepDecrypterKeyPasswordFileFlag, // Cloud provisioner flags awsAccountFlag, @@ -146,9 +176,12 @@ SCEP removeGCPServiceAccountFlag, gcpProjectFlag, removeGCPProjectFlag, + gcpOrganizationFlag, instanceAgeFlag, disableCustomSANsFlag, disableTOFUFlag, + disableSSHCAUserFlag, + disableSSHCAHostFlag, // Claims x509TemplateFlag, @@ -166,6 +199,7 @@ SCEP sshHostDefaultDurFlag, disableRenewalFlag, allowRenewalAfterExpiryFlag, + disableSmallstepExtensionsFlag, //enableX509Flag, enableSSHFlag, @@ -257,6 +291,18 @@ $ step ca provisioner update Google \ --disable-custom-sans --gcp-project internal --remove-gcp-project public ''' +Remove the GCP project and use an organization id: +''' +$ step ca provisioner update Google \ + --gpc-organization 123456789 --remove-gcp-project internal +''' + +Remove the GCP organization and use a project: +''' +$ step ca provisioner update Google \ + --gpc-organization="" --gcp-project internal +''' + Update an AWS provisioner: ''' $ step ca provisioner update Amazon --disable-custom-sans --disable-trust-on-first-use @@ -335,11 +381,7 @@ func updateAction(ctx *cli.Context) (err error) { return err } - if err := client.UpdateProvisioner(name, p); err != nil { - return err - } - - return nil + return client.UpdateProvisioner(name, p) } func updateTemplates(ctx *cli.Context, p *linkedca.Provisioner) error { @@ -408,8 +450,11 @@ func updateClaims(ctx *cli.Context, p *linkedca.Provisioner) { if ctx.IsSet("allow-renewal-after-expiry") { p.Claims.AllowRenewalAfterExpiry = ctx.Bool("allow-renewal-after-expiry") } - claims := p.Claims + if ctx.IsSet("disable-smallstep-extensions") { + p.Claims.DisableSmallstepExtensions = ctx.Bool("disable-smallstep-extensions") + } + claims := p.Claims if claims.X509 == nil { claims.X509 = &linkedca.X509Claims{} } @@ -478,7 +523,7 @@ func updateJWKDetails(ctx *cli.Context, p *linkedca.Provisioner) error { err error password string ) - if passwordFile := ctx.String("password-file"); len(passwordFile)> 0 { + if passwordFile := ctx.String("password-file"); passwordFile != "" { password, err = utils.ReadStringPasswordFromFile(passwordFile) if err != nil { return err @@ -639,7 +684,7 @@ func updateACMEDetails(ctx *cli.Context, p *linkedca.Provisioner) error { return nil } -func updateSSHPOPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { +func updateSSHPOPDetails(*cli.Context, *linkedca.Provisioner) error { return nil } @@ -783,10 +828,29 @@ func updateOIDCDetails(ctx *cli.Context, p *linkedca.Provisioner) error { } details.ConfigurationEndpoint = ce } + if ctx.IsSet("remove-scope") { + details.Scopes = removeElements(details.Scopes, ctx.StringSlice("remove-scope")) + } + if ctx.IsSet("scope") { + details.Scopes = append(details.Scopes, ctx.StringSlice("scope")...) + } + if ctx.IsSet("remove-auth-param") { + details.AuthParams = removeElements(details.AuthParams, ctx.StringSlice("remove-auth-param")) + } + if ctx.IsSet("auth-param") { + details.AuthParams = append(details.AuthParams, ctx.StringSlice("auth-param")...) + } return nil } func updateAWSDetails(ctx *cli.Context, p *linkedca.Provisioner) error { + if ctx.IsSet("disable-ssh-ca-user") { + return errors.New("flag disable-ssh-ca-user is not supported for AWS IID provisioners") + } + if ctx.IsSet("disable-ssh-ca-host") { + return errors.New("flag disable-ssh-ca-host is not supported for AWS IID provisioners") + } + data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_AWS) if !ok { return errors.New("error casting details to AWS type") @@ -804,7 +868,7 @@ func updateAWSDetails(ctx *cli.Context, p *linkedca.Provisioner) error { details.DisableCustomSans = ctx.Bool("disable-custom-sans") } if ctx.IsSet("disable-trust-on-first-use") { - details.DisableCustomSans = ctx.Bool("disable-trust-on-first-use") + details.DisableTrustOnFirstUse = ctx.Bool("disable-trust-on-first-use") } if ctx.IsSet("remove-aws-account") { details.Accounts = removeElements(details.Accounts, ctx.StringSlice("remove-aws-account")) @@ -816,6 +880,13 @@ func updateAWSDetails(ctx *cli.Context, p *linkedca.Provisioner) error { } func updateAzureDetails(ctx *cli.Context, p *linkedca.Provisioner) error { + if ctx.IsSet("disable-ssh-ca-user") { + return errors.New("flag disable-ssh-ca-user is not supported for Azure IID provisioners") + } + if ctx.IsSet("disable-ssh-ca-host") { + return errors.New("flag disable-ssh-ca-host is not supported for Azure IID provisioners") + } + data, ok := p.Details.GetData().(*linkedca.ProvisionerDetails_Azure) if !ok { return errors.New("error casting details to Azure type") @@ -832,7 +903,7 @@ func updateAzureDetails(ctx *cli.Context, p *linkedca.Provisioner) error { details.DisableCustomSans = ctx.Bool("disable-custom-sans") } if ctx.IsSet("disable-trust-on-first-use") { - details.DisableCustomSans = ctx.Bool("disable-trust-on-first-use") + details.DisableTrustOnFirstUse = ctx.Bool("disable-trust-on-first-use") } if ctx.IsSet("remove-azure-resource-group") { details.ResourceGroups = removeElements(details.ResourceGroups, ctx.StringSlice("remove-azure-resource-group")) @@ -873,7 +944,15 @@ func updateGCPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { details.DisableCustomSans = ctx.Bool("disable-custom-sans") } if ctx.IsSet("disable-trust-on-first-use") { - details.DisableCustomSans = ctx.Bool("disable-trust-on-first-use") + details.DisableTrustOnFirstUse = ctx.Bool("disable-trust-on-first-use") + } + if ctx.IsSet("disable-ssh-ca-user") { + boolVal := ctx.Bool("disable-ssh-ca-user") + details.DisableSshCaUser = &boolVal + } + if ctx.IsSet("disable-ssh-ca-host") { + boolVal := ctx.Bool("disable-ssh-ca-host") + details.DisableSshCaHost = &boolVal } if ctx.IsSet("remove-gcp-service-account") { details.ServiceAccounts = removeElements(details.ServiceAccounts, ctx.StringSlice("remove-gcp-service-account")) @@ -881,12 +960,21 @@ func updateGCPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { if ctx.IsSet("gcp-service-account") { details.ServiceAccounts = append(details.ServiceAccounts, ctx.StringSlice("gcp-service-account")...) } + if ctx.IsSet("gcp-organization") { + details.OrganizationId = ctx.String("gcp-organization") + } if ctx.IsSet("remove-gcp-project") { details.ProjectIds = removeElements(details.ProjectIds, ctx.StringSlice("remove-gcp-project")) } if ctx.IsSet("gcp-project") { details.ProjectIds = append(details.ProjectIds, ctx.StringSlice("gcp-project")...) } + + // Validate configuration + if details.OrganizationId != "" && len(details.ProjectIds)> 0 { + return errs.IncompatibleFlagWithFlag(ctx, "gcp-organization", "gcp-project") + } + return nil } @@ -907,13 +995,50 @@ func updateSCEPDetails(ctx *cli.Context, p *linkedca.Provisioner) error { details.Capabilities = ctx.StringSlice("capabilities") } if ctx.IsSet("min-public-key-length") { - details.MinimumPublicKeyLength = int32(ctx.Int("min-public-key-length")) + details.MinimumPublicKeyLength = cast.Int32(ctx.Int("min-public-key-length")) } if ctx.IsSet("include-root") { details.IncludeRoot = ctx.Bool("include-root") } + if ctx.IsSet("exclude-intermediate") { + details.ExcludeIntermediate = ctx.Bool("exclude-intermediate") + } if ctx.IsSet("encryption-algorithm-identifier") { - details.EncryptionAlgorithmIdentifier = int32(ctx.Int("encryption-algorithm-identifier")) + details.EncryptionAlgorithmIdentifier = cast.Int32(ctx.Int("encryption-algorithm-identifier")) + } + + decrypter := details.GetDecrypter() + if decrypter == nil { + decrypter = &linkedca.SCEPDecrypter{} + } + if ctx.IsSet("scep-decrypter-certificate-file") { + decrypterCertificateFile := ctx.String("scep-decrypter-certificate-file") + data, err := parseSCEPDecrypterCertificate(decrypterCertificateFile) + if err != nil { + return fmt.Errorf("failed parsing certificate from %q: %w", decrypterCertificateFile, err) + } + decrypter.Certificate = data + details.Decrypter = decrypter + } + if ctx.IsSet("scep-decrypter-key-uri") { + decrypter.KeyUri = ctx.String("scep-decrypter-key-uri") + details.Decrypter = decrypter + } + if decrypterKeyFile := ctx.String("scep-decrypter-key-file"); decrypterKeyFile != "" { + data, err := readSCEPDecrypterKey(decrypterKeyFile) + if err != nil { + return fmt.Errorf("failed reading decrypter key from %q: %w", decrypterKeyFile, err) + } + decrypter.Key = data + details.Decrypter = decrypter + } + if decrypterKeyPasswordFile := ctx.String("scep-decrypter-key-password-file"); decrypterKeyPasswordFile != "" { + decrypterKeyPassword, err := utils.ReadPasswordFromFile(decrypterKeyPasswordFile) + if err != nil { + return fmt.Errorf("failed reading decrypter key password from %q: %w", decrypterKeyPasswordFile, err) + } + decrypter.KeyPassword = decrypterKeyPassword + details.Decrypter = decrypter } return nil diff --git a/command/ca/provisioner/webhook/add.go b/command/ca/provisioner/webhook/add.go index 5da01743..d6dda214 100644 --- a/command/ca/provisioner/webhook/add.go +++ b/command/ca/provisioner/webhook/add.go @@ -3,11 +3,13 @@ package webhook import ( "fmt" + "github.com/urfave/cli" + + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) func addCommand() cli.Command { diff --git a/command/ca/provisioner/webhook/remove.go b/command/ca/provisioner/webhook/remove.go index 83ad647b..26b03780 100644 --- a/command/ca/provisioner/webhook/remove.go +++ b/command/ca/provisioner/webhook/remove.go @@ -1,9 +1,11 @@ package webhook import ( - "github.com/smallstep/cli/flags" "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" + + "github.com/smallstep/cli-utils/errs" + + "github.com/smallstep/cli/flags" ) func removeCommand() cli.Command { @@ -59,9 +61,5 @@ func removeAction(ctx *cli.Context) (err error) { return err } - if err := client.DeleteProvisionerWebhook(provisionerName, args.Get(1)); err != nil { - return err - } - - return nil + return client.DeleteProvisionerWebhook(provisionerName, args.Get(1)) } diff --git a/command/ca/provisioner/webhook/update.go b/command/ca/provisioner/webhook/update.go index 95f17bbd..abcbbdac 100644 --- a/command/ca/provisioner/webhook/update.go +++ b/command/ca/provisioner/webhook/update.go @@ -4,12 +4,14 @@ import ( "errors" "fmt" + "github.com/urfave/cli" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/linkedca" ) func updateCommand() cli.Command { diff --git a/command/ca/provisioner/webhook/webhook.go b/command/ca/provisioner/webhook/webhook.go index be339194..b4ac65a2 100644 --- a/command/ca/provisioner/webhook/webhook.go +++ b/command/ca/provisioner/webhook/webhook.go @@ -5,13 +5,15 @@ import ( "fmt" "os" + "github.com/urfave/cli" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + "github.com/smallstep/linkedca" + "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/linkedca" ) // Command returns the webhook subcommand. diff --git a/command/ca/rekey.go b/command/ca/rekey.go index dfb9b9e9..aa80dcac 100644 --- a/command/ca/rekey.go +++ b/command/ca/rekey.go @@ -10,15 +10,18 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/pki" - "github.com/smallstep/cli/flags" - "github.com/smallstep/cli/utils" "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" + + "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" + + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cryptoutil" + "github.com/smallstep/cli/utils" ) func rekeyCertificateCommand() cli.Command { @@ -30,7 +33,7 @@ func rekeyCertificateCommand() cli.Command { [**--out-cert**=] [**--out-key**=] [**--private-key**=] [**--ca-url**=] [**--root**=] [**--password-file**=] [**--expires-in**=] [**--force**] [**--exec**=] [**--daemon**] -[**--kty**=] [**--curve**=] [**--size**=] +[**--kms**=] [**--kty**=] [**--curve**=] [**--size**=] [**--expires-in**=] [**--pid**=] [**--pid-file**=] [**--signal**=] [**--exec**=] [**--rekey-period**=]`, Description: ` @@ -50,6 +53,10 @@ fixed period can be set with the **--rekey-period** flag. The **--daemon** flag can be combined with **--pid**, **--signal**, or **--exec** to provide certificate reloads on your services. +The **--kms** flag rekeys an existing key in a KMS with another key from the same +KMS. It does not support generating new keys, using the **--daemon** flag, or +rekeying across different KMS instances. + ## POSITIONAL ARGUMENTS @@ -76,6 +83,24 @@ Rekey a certificate forcing the overwrite of the previous certificate and key $ step ca rekey --force internal.crt internal.key ''' +Rekey a certificate using a KMS, with another from the same KMS: +''' +$ step ca rekey --private-key 'yubikey:slot-id=9a?pin-value=123456' \ + yubikey.crt 'yubikey:slot-id=82?pin-value=123456' +''' + +Rekey a certificate using a KMS with the <--kms> flag: +''' +$ step ca rekey \ + --kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \ + --private-key 'pkcs11:id=4002' pkcs11.crt 'pkcs11:id=4001' +''' + +''' +$ step ca rekey --key yubikey:pin-value=123456 --private-key yubikey:slot-id=9a \ + yubikey.crt 'yubikey:slot-id=82 +''' + Rekey a certificate providing the <--ca-url> and <--root> flags: ''' $ step ca rekey --ca-url https://ca.smallstep.com:9000 \ @@ -192,6 +217,7 @@ Requires the **--daemon** flag. The is a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "1.5h", or "2h45m". Valid time units are "ns", "us" (or "μs"), "ms", "s", "m", "h".`, }, + flags.KMSUri, flags.KTY, flags.Curve, flags.Size, @@ -218,6 +244,21 @@ func rekeyCertificateAction(ctx *cli.Context) error { isDaemon := ctx.Bool("daemon") execCmd := ctx.String("exec") givenPrivate := ctx.String("private-key") + kmsURI := ctx.String("kms") + + // For now, if the --kms flag is given, do not allow to generate a new key + // and write it on disk. We can't use the daemon mode because we + // cannot generate new keys. + if kmsURI != "" || cryptoutil.IsKMS(keyFile) { + switch { + case givenPrivate == "": + return errs.RequiredWithFlag(ctx, "kms", "private-key") + case ctx.IsSet("out-key"): + return errs.IncompatibleFlagWithFlag(ctx, "kms", "out-key") + case isDaemon: + return errs.IncompatibleFlagWithFlag(ctx, "kms", "daemon") + } + } outCert := ctx.String("out-cert") if outCert == "" { @@ -239,12 +280,12 @@ func rekeyCertificateAction(ctx *cli.Context) error { } var expiresIn, rekeyPeriod time.Duration - if s := ctx.String("expires-in"); len(s)> 0 { + if s := ctx.String("expires-in"); s != "" { if expiresIn, err = time.ParseDuration(s); err != nil { return errs.InvalidFlagValue(ctx, "expires-in", s, "") } } - if s := ctx.String("rekey-period"); len(s)> 0 { + if s := ctx.String("rekey-period"); s != "" { if rekeyPeriod, err = time.ParseDuration(s); err != nil { return errs.InvalidFlagValue(ctx, "rekey-period", s, "") } @@ -265,7 +306,7 @@ func rekeyCertificateAction(ctx *cli.Context) error { } pidFile := ctx.String("pid-file") - if len(pidFile)> 0 { + if pidFile != "" { pidB, err := os.ReadFile(pidFile) if err != nil { return errs.FileError(err, pidFile) @@ -284,7 +325,7 @@ func rekeyCertificateAction(ctx *cli.Context) error { return errs.InvalidFlagValue(ctx, "signal", strconv.Itoa(signum), "") } - cert, err := tlsLoadX509KeyPair(certFile, keyFile, passFile) + cert, err := tlsLoadX509KeyPair(kmsURI, certFile, keyFile, passFile) if err != nil { return err } @@ -322,23 +363,27 @@ func rekeyCertificateAction(ctx *cli.Context) error { } } - var priv crypto.PrivateKey + var signer crypto.Signer if givenPrivate == "" { kty, crv, size, err := utils.GetKeyDetailsFromCLI(ctx, false, "kty", "curve", "size") if err != nil { return err } - priv, err = keyutil.GenerateKey(kty, crv, size) + signer, err = keyutil.GenerateSigner(kty, crv, size) if err != nil { return err } } else { - priv, err = pemutil.Read(givenPrivate) + opts := []pemutil.Options{pemutil.WithFilename(givenPrivate)} + if passFile != "" { + opts = append(opts, pemutil.WithPasswordFile(passFile)) + } + signer, err = cryptoutil.CreateSigner(kmsURI, givenPrivate, opts...) if err != nil { return err } } - if _, err := renewer.Rekey(priv, outCert, outKey, ctx.IsSet("out-key") || givenPrivate == ""); err != nil { + if _, err := renewer.Rekey(signer, outCert, outKey, ctx.IsSet("out-key") || givenPrivate == ""); err != nil { return err } diff --git a/command/ca/renew.go b/command/ca/renew.go index cbe4ef51..b26b6be8 100644 --- a/command/ca/renew.go +++ b/command/ca/renew.go @@ -20,21 +20,24 @@ import ( "time" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/fileutil" + "github.com/smallstep/cli-utils/ui" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/x509util" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cryptoutil" "github.com/smallstep/cli/token" - "github.com/smallstep/cli/utils" "github.com/smallstep/cli/utils/cautils" "github.com/smallstep/cli/utils/sysutils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/jose" - "go.step.sm/crypto/pemutil" - "go.step.sm/crypto/x509util" ) func renewCertificateCommand() cli.Command { @@ -45,7 +48,7 @@ func renewCertificateCommand() cli.Command { UsageText: `**step ca renew** [**--mtls**] [**--password-file**=] [**--out**=] [**--expires-in**=] [**--force**] [**--pid**=] [**--pid-file**=] [**--signal**=] -[**--exec**=] [**--daemon**] [**--renew-period**=] +[**--exec**=] [**--daemon**] [**--renew-period**=] [**--kms**=] [**--ca-url**=] [**--root**=] [**--context**=]`, Description: ` **step ca renew** command renews the given certificate (with a request to the @@ -102,6 +105,18 @@ Renew a certificate using the token flow instead of mTLS: $ step ca renew --mtls=false --force internal.crt internal.key ''' +Renew a certificate which key is in a KMS: +''' +$ step ca renew yubikey.crt 'yubikey:slot-id=9a?pin-value=123456' +''' + +Renew a certificate which key is in a KMS, using the <--kms> flag: +''' +$ step ca renew \ + --kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \ + pkcs11.crt 'pkcs11:id=4001' +''' + Renew a certificate providing the <--ca-url> and <--root> flags: ''' $ step ca renew --ca-url https://ca.smallstep.com:9000 \ @@ -157,6 +172,7 @@ authorization flow instead.`, flags.Force, flags.Offline, flags.PasswordFile, + flags.KMSUri, cli.StringFlag{ Name: "out,output-file", Usage: "The new certificate path. Defaults to overwriting the positional argument", @@ -226,6 +242,7 @@ func renewCertificateAction(ctx *cli.Context) error { passFile := ctx.String("password-file") isDaemon := ctx.Bool("daemon") execCmd := ctx.String("exec") + kmsURI := ctx.String("kms") outFile := ctx.String("out") if outFile == "" { @@ -243,12 +260,12 @@ func renewCertificateAction(ctx *cli.Context) error { } var expiresIn, renewPeriod time.Duration - if s := ctx.String("expires-in"); len(s)> 0 { + if s := ctx.String("expires-in"); s != "" { if expiresIn, err = time.ParseDuration(s); err != nil { return errs.InvalidFlagValue(ctx, "expires-in", s, "") } } - if s := ctx.String("renew-period"); len(s)> 0 { + if s := ctx.String("renew-period"); s != "" { if renewPeriod, err = time.ParseDuration(s); err != nil { return errs.InvalidFlagValue(ctx, "renew-period", s, "") } @@ -269,7 +286,7 @@ func renewCertificateAction(ctx *cli.Context) error { } pidFile := ctx.String("pid-file") - if len(pidFile)> 0 { + if pidFile != "" { pidB, err := os.ReadFile(pidFile) if err != nil { return errs.FileError(err, pidFile) @@ -288,7 +305,7 @@ func renewCertificateAction(ctx *cli.Context) error { return errs.InvalidFlagValue(ctx, "signal", strconv.Itoa(signum), "") } - cert, err := tlsLoadX509KeyPair(certFile, keyFile, passFile) + cert, err := tlsLoadX509KeyPair(kmsURI, certFile, keyFile, passFile) if err != nil { return err } @@ -472,7 +489,7 @@ func (r *renewer) Renew(outFile string) (resp *api.SignResponse, err error) { return nil, errors.Wrap(err, "error renewing certificate") } - if resp.CertChainPEM == nil || len(resp.CertChainPEM) == 0 { + if len(resp.CertChainPEM) == 0 { resp.CertChainPEM = []api.Certificate{resp.ServerPEM, resp.CaPEM} } var data []byte @@ -483,7 +500,7 @@ func (r *renewer) Renew(outFile string) (resp *api.SignResponse, err error) { } data = append(data, pem.EncodeToMemory(pemblk)...) } - if err := utils.WriteFile(outFile, data, 0600); err != nil { + if err := fileutil.WriteFile(outFile, data, 0o600); err != nil { return nil, errs.FileError(err, outFile) } @@ -503,7 +520,7 @@ func (r *renewer) Rekey(priv interface{}, outCert, outKey string, writePrivateKe if err != nil { return nil, errors.Wrap(err, "error rekeying certificate") } - if resp.CertChainPEM == nil || len(resp.CertChainPEM) == 0 { + if len(resp.CertChainPEM) == 0 { resp.CertChainPEM = []api.Certificate{resp.ServerPEM, resp.CaPEM} } var data []byte @@ -514,11 +531,11 @@ func (r *renewer) Rekey(priv interface{}, outCert, outKey string, writePrivateKe } data = append(data, pem.EncodeToMemory(pemblk)...) } - if err := utils.WriteFile(outCert, data, 0600); err != nil { + if err := fileutil.WriteFile(outCert, data, 0o600); err != nil { return nil, errs.FileError(err, outCert) } if writePrivateKey { - _, err = pemutil.Serialize(priv, pemutil.ToFile(outKey, 0600)) + _, err = pemutil.Serialize(priv, pemutil.ToFile(outKey, 0o600)) if err != nil { return nil, err } @@ -636,7 +653,7 @@ func (r *renewer) RenewWithToken(cert tls.Certificate) (*api.SignResponse, error return r.client.RenewWithToken(tok) } -func tlsLoadX509KeyPair(certFile, keyFile, passFile string) (tls.Certificate, error) { +func tlsLoadX509KeyPair(kms, certFile, keyFile, passFile string) (tls.Certificate, error) { x509Chain, err := pemutil.ReadCertificateBundle(certFile) if err != nil { return tls.Certificate{}, errs.Wrap(err, "error reading certificate chain") @@ -650,14 +667,13 @@ func tlsLoadX509KeyPair(certFile, keyFile, passFile string) (tls.Certificate, er if passFile != "" { opts = append(opts, pemutil.WithPasswordFile(passFile)) } - pk, err := pemutil.Read(keyFile, opts...) + signer, err := cryptoutil.CreateSigner(kms, keyFile, opts...) if err != nil { - return tls.Certificate{}, errs.Wrap(err, "error parsing private key") + return tls.Certificate{}, errs.Wrap(err, "error loading private key") } - return tls.Certificate{ Certificate: x509ChainBytes, - PrivateKey: pk, + PrivateKey: signer, Leaf: x509Chain[0], }, nil } diff --git a/command/ca/revoke.go b/command/ca/revoke.go index 1cbb1fe8..2d5415ee 100644 --- a/command/ca/revoke.go +++ b/command/ca/revoke.go @@ -13,20 +13,22 @@ import ( "time" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/pki" - "github.com/smallstep/cli/flags" - "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" "golang.org/x/crypto/ocsp" + + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/utils/cautils" ) /* @@ -43,7 +45,7 @@ func revokeCertificateCommand() cli.Command { Usage: "revoke a certificate", UsageText: `**step ca revoke** [**--cert**=] [**--key**=] [**--token**=] -[**--reason**=] [**--reasonCode**=] [**-offline**] +[**--reason**=] [**--reasonCode**=] [**--offline**] [**--ca-url**=] [**--root**=] [**--context**=]`, Description: ` **step ca revoke** command revokes a certificate with the given serial @@ -64,9 +66,6 @@ can verify certificates in a simple, decentralized manner without relying on centralized 3rd parties. Passive revocation works best with short certificate lifetimes. -**step ca revoke** currently only supports passive revocation. Active revocation -is on our roadmap. - A revocation request can be authorized using a JWK provisioner token, or using a client certificate. @@ -234,7 +233,7 @@ func revokeCertificateAction(ctx *cli.Context) error { // If cert and key are passed then infer the serial number and certificate // that should be revoked. - if len(certFile)> 0 || len(keyFile)> 0 { + if certFile != "" || keyFile != "" { // Must be using cert/key flags for mTLS revoke so should be 0 cmd line args. if ctx.NArg()> 0 { return errors.Errorf("'%s %s --cert --key ' expects no additional positional arguments", ctx.App.Name, ctx.Command.Name) @@ -245,10 +244,10 @@ func revokeCertificateAction(ctx *cli.Context) error { if keyFile == "" { return errs.RequiredWithFlag(ctx, "cert", "key") } - if len(token)> 0 { + if token != "" { errs.IncompatibleFlagWithFlag(ctx, "cert", "token") } - if len(serial)> 0 { + if serial != "" { errs.IncompatibleFlagWithFlag(ctx, "cert", "serial") } var cert []*x509.Certificate @@ -309,7 +308,7 @@ func newRevokeFlow(ctx *cli.Context, certFile, keyFile string) (*revokeFlow, err if err != nil { return nil, err } - if len(certFile)> 0 || len(keyFile)> 0 { + if certFile != "" || keyFile != "" { if err := offlineClient.VerifyClientCert(certFile, keyFile); err != nil { return nil, err } @@ -335,7 +334,7 @@ func (f *revokeFlow) getClient(ctx *cli.Context, serial, token string) (cautils. rootFile := ctx.String("root") var options []ca.ClientOption - if len(token)> 0 { + if token != "" { tok, err := jose.ParseSigned(token) if err != nil { return nil, errors.Wrap(err, "error parsing flag '--token'") @@ -349,7 +348,7 @@ func (f *revokeFlow) getClient(ctx *cli.Context, serial, token string) (cautils. } // Prepare client for bootstrap or provisioning tokens - if len(claims.SHA)> 0 && len(claims.Audience)> 0 && strings.HasPrefix(strings.ToLower(claims.Audience[0]), "http") { + if claims.SHA != "" && len(claims.Audience)> 0 && strings.HasPrefix(strings.ToLower(claims.Audience[0]), "http") { if caURL == "" { caURL = claims.Audience[0] } diff --git a/command/ca/root.go b/command/ca/root.go index 9e1ca03c..9bbaaa2c 100644 --- a/command/ca/root.go +++ b/command/ca/root.go @@ -6,14 +6,15 @@ import ( "strings" "github.com/pkg/errors" + "github.com/urfave/cli" "github.com/smallstep/certificates/ca" - "github.com/smallstep/cli/flags" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" "go.step.sm/crypto/pemutil" + + "github.com/smallstep/cli/flags" ) func rootCommand() cli.Command { diff --git a/command/ca/sign.go b/command/ca/sign.go index e973d3b4..5129aa89 100644 --- a/command/ca/sign.go +++ b/command/ca/sign.go @@ -5,22 +5,24 @@ import ( "strings" "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/api" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/ui" + "go.step.sm/crypto/pemutil" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/token" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" - "go.step.sm/cli-utils/ui" - "go.step.sm/crypto/pemutil" ) func signCertificateCommand() cli.Command { return cli.Command{ Name: "sign", Action: command.ActionFunc(signCertificateAction), - Usage: "generate a new certificate signing a certificate request", + Usage: "generate a new certificate from signing a certificate request", UsageText: `**step ca sign** [**--token**=] [**--issuer**=] [**--provisioner-password-file=] [**--not-before**=] [**--not-after**=] @@ -124,7 +126,7 @@ $ step ca sign foo.csr foo.crt \ flags.Force, flags.Offline, flags.PasswordFile, - consoleFlag, + flags.Console, flags.KMSUri, flags.X5cCert, flags.X5cKey, @@ -175,7 +177,7 @@ func signCertificateAction(ctx *cli.Context) error { } // certificate flow unifies online and offline flows on a single api - flow, err := cautils.NewCertificateFlow(ctx) + flow, err := cautils.NewCertificateFlow(ctx, cautils.WithCertificateRequest(csr)) if err != nil { return err } diff --git a/command/ca/token.go b/command/ca/token.go index 96a089b3..e074171c 100644 --- a/command/ca/token.go +++ b/command/ca/token.go @@ -4,23 +4,23 @@ import ( "fmt" "os" + "github.com/pkg/errors" + "github.com/urfave/cli" + "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/pki" + "github.com/smallstep/cli-utils/command" + "github.com/smallstep/cli-utils/errs" + "github.com/smallstep/cli-utils/fileutil" + "go.step.sm/crypto/pemutil" + "golang.org/x/crypto/ssh" + "github.com/smallstep/cli/flags" "github.com/smallstep/cli/utils" "github.com/smallstep/cli/utils/cautils" - "github.com/urfave/cli" - "go.step.sm/cli-utils/command" - "go.step.sm/cli-utils/errs" ) func tokenCommand() cli.Command { - // Avoid the conflict with --not-before --not-after - certNotBeforeFlag := flags.NotBefore - certNotAfterFlag := flags.NotAfter - certNotBeforeFlag.Name = "cert-not-before" - certNotAfterFlag.Name = "cert-not-after" - return cli.Command{ Name: "token", Action: command.ActionFunc(tokenAction), @@ -30,11 +30,13 @@ func tokenCommand() cli.Command { [**--cert-not-before**=] [**--cert-not-after**=] [**--not-before**=] [**--not-after**=] [**--password-file**=] [**--provisioner-password-file**=] -[**--output-file**=] [**--key**=] [**--san**=] [**--offline**] +[**--output-file**=] [**--kms**=uri] [**--key**=] [**--san**=] [**--offline**] [**--revoke**] [**--x5c-cert**=] [**--x5c-key**=] [**--x5c-insecure**] [**--sshpop-cert**=] [**--sshpop-key**=] +[**--cnf**=] [**--cnf-file**=] [**--ssh**] [**--host**] [**--principal**=] [**--k8ssa-token-path**=] -[**--ca-url**=] [**--root**=] [**--context**=]`, +[**--ca-url**=] [**--root**=] [**--context**=] +[**--set**=] [**--set-file**=]`, Description: `**step ca token** command generates a one-time token granting access to the certificates authority. @@ -88,6 +90,18 @@ Get a new token that becomes valid in 30 minutes and expires 5 minutes after tha $ step ca token --not-before 30m --not-after 35m internal.example.com ''' +Get a new token with a confirmation claim to enforce a given CSR fingerprint: +''' +$ step certificate fingerprint --format base64-url-raw internal.csr +PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw +$ step ca token --cnf PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw internal.smallstep.com +''' + +Get a new token with a confirmation claim to enforce the use of a given CSR: +''' +step ca token --cnf-file internal.csr internal.smallstep.com +''' + Get a new token signed with the given private key, the public key must be configured in the certificate authority: ''' @@ -139,14 +153,44 @@ Get a new token for an SSH host certificate: $ step ca token my-remote.hostname --ssh --host ''' +Get a new token with a confirmation claim to enforce the use of a given public key: +''' +step ca token --ssh --host --cnf-file internal.pub internal.smallstep.com +''' + Generate a renew token and use it in a renew after expiry request: ''' $ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com) $ curl -X POST -H "Authorization: Bearer $TOKEN" https://ca.example.com/1.0/renew +''' + +Generate a JWK provisioner token using a key in a YubiKey: +''' +$ step ca token --kms yubikey:pin-value=123456 --key yubikey:slot-id=82 internal.example.com +''' + +Generate an X5C provisioner token using a certificate in a YubiKey. Note that a +YubiKey does not support storing a certificate bundle. To make it work, you must +add the intermediate and the root in the provisioner configuration: +''' +$ step ca token \ + --x5c-cert yubikey:slot-id=82 \ + --x5c-key 'yubikey:slot-id=82?pin=value=123456' \ + internal.example.com +''' + +Generate a token with custom data in the "user" claim. The example below can be +accessed in a template as **.Token.user.field**, rendering to the string +"value". + +This is distinct from **.Insecure.User**: any attributes set using this option +are added to a claim named "user" in the signed JWT produced by this command. +This data may therefore be considered trusted (insofar as the token itself is +trusted). +''' +$ step ca token --set field=value internal.example.com '''`, Flags: []cli.Flag{ - certNotAfterFlag, - certNotBeforeFlag, provisionerKidFlag, cli.StringSliceFlag{ Name: "san", @@ -165,11 +209,28 @@ multiple principals.`, sshHostFlag, flags.CaConfig, flags.Force, - flags.NotAfter, - flags.NotBefore, + cli.StringFlag{ + Name: "not-before", + Usage: `The when the token's validity period starts. If a