diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 64a3fa8e3..70ed5c05b 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -2,7 +2,7 @@ version: 2 updates: - package-ecosystem: "docker" - directory: "/" + directory: "/.release" labels: ["dependencies"] schedule: # By default, this will be on a Monday. @@ -39,3 +39,16 @@ updates: go: patterns: - "*" + + - package-ecosystem: "cargo" + directory: "/functional-tests" + labels: ["area/CI", "dependencies"] + schedule: + # By default, this will be on a Monday. + interval: "weekly" + groups: + # Group all updates together, so that they are all applied in a single PR. + # xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups + rust: + patterns: + - "*" diff --git a/.github/utils/patch-go.mod.py b/.github/utils/patch-go.mod.py new file mode 100644 index 000000000..892dfbba4 --- /dev/null +++ b/.github/utils/patch-go.mod.py @@ -0,0 +1,77 @@ +""" +Patch go.mod so that the lines 'go xxx' to 'toolchain xxx' are as in git's +HEAD. + +This is necessary since newer 'go mod tidy' versions tend to modify these +lines. Since we check in CI that 'go mod tidy' does not change go.mod, this +causes CI to fail. +""" + +import subprocess + + +def split_go_mod(contents: str) -> tuple[list[str], list[str], list[str]]: + """ + Given the contents of go.mod, splits it into three lists of lines + (with endings): + 1. The lines before 'go'; + 2. The lines starting with 'go' and ending with 'toolchain'; + 3. The lines after 'toolchain'. + """ + parts: tuple[list[str], list[str], list[str]] = ([], [], []) + index = 0 + for line in contents.splitlines(keepends=True): + next_index = index + if line.startswith('go '): + index = next_index = 1 + if line.startswith('toolchain '): + next_index = 2 + parts[index].append(line) + index = next_index + return parts + + +def get_file_contents_from_git_revision(filename: str, revision: str) -> str: + """ + Get the file contents of ``filename`` from Git revision ``revision``. + """ + p = subprocess.run( + ['git', 'show', f'{revision}:{filename}'], + stdout=subprocess.PIPE, + check=True, + encoding='utf-8', + ) + return p.stdout + + +def read_file(filename: str) -> str: + """ + Read the file's contents. + """ + with open(filename, 'r', encoding='utf-8') as f: + return f.read() + + +def write_file(filename: str, contents: str) -> None: + """ + Write the file's contents. + """ + with open(filename, 'w', encoding='utf-8') as f: + f.write(contents) + + +def main(): + """ + Patches go.mod. + """ + filename = 'go.mod' + _, go_versions, __ = split_go_mod( + get_file_contents_from_git_revision(filename, 'HEAD') + ) + head, _, tail = split_go_mod(read_file(filename)) + lines = head + go_versions + tail + write_file(filename, ''.join(lines)) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 4ca418ff3..40d2ee679 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -13,30 +13,33 @@ permissions: jobs: build: - name: Build and test ${{ matrix.os }} ${{ matrix.arch }} + name: Build and test ${{ matrix.os }} ${{ matrix.arch }} ${{ matrix.go-version }} runs-on: ubuntu-latest strategy: matrix: os: [linux, darwin, windows] arch: [amd64, arm64] + go-version: ['1.22', '1.23'] exclude: - os: windows arch: arm64 env: - VAULT_VERSION: "1.1.3" + VAULT_VERSION: "1.14.0" VAULT_TOKEN: "root" VAULT_ADDR: "http://127.0.0.1:8200" steps: - - name: Set up Go 1.21 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: - go-version: '1.21' + go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -46,16 +49,19 @@ jobs: - name: Vendor Go Modules run: make vendor + - name: Restore go/toolchain lines of go.mod + run: python3 .github/utils/patch-go.mod.py + - name: Ensure clean working tree run: git diff --exit-code - - name: Build Linux and Darwin + - name: Build ${{ matrix.os }} if: matrix.os != 'windows' - run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} -v ./cmd/sops + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} -v ./cmd/sops - - name: Build Windows + - name: Build ${{ matrix.os }} if: matrix.os == 'windows' - run: GOOS=${{ matrix.os }} go build -o sops-${{ matrix.os }}-${{ github.sha }} -v ./cmd/sops + run: GOOS=${{ matrix.os }} go build -o sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ github.sha }} -v ./cmd/sops - name: Import test GPG keys run: for i in 1 2 3 4 5; do gpg --import pgp/sops_functional_tests_key.asc && break || sleep 15; done @@ -63,40 +69,51 @@ jobs: - name: Test run: make test - - name: Upload artifact for Linux and Darwin + - name: Upload artifact for ${{ matrix.os }} if: matrix.os != 'windows' - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} - path: sops-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} + name: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} + path: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} - - name: Upload artifact for Windows + - name: Upload artifact for ${{ matrix.os }} if: matrix.os == 'windows' - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sops-${{ matrix.os }}-${{ github.sha }} - path: sops-${{ matrix.os }}-${{ github.sha }} + name: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ github.sha }} + path: sops-${{ matrix.go-version }}-${{ matrix.os }}-${{ github.sha }} test: name: Functional tests runs-on: ubuntu-latest needs: [build] + strategy: + matrix: + go-version: ['1.22'] env: - VAULT_VERSION: "1.1.3" + VAULT_VERSION: "1.14.0" VAULT_TOKEN: "root" VAULT_ADDR: "http://127.0.0.1:8200" steps: - - name: Install rustup - run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y --default-toolchain 1.70.0 - - name: Check out code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - name: sops-linux-amd64-${{ github.sha }} + persist-credentials: false + + # Rustup will detect toolchain version and profile from rust-toolchain.toml + # It will download and install the toolchain and components automatically + # and make them available for subsequent commands + - name: Install Rust toolchain + run: rustup show + + - name: Show Rust version + run: cargo --version + + - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + name: sops-${{ matrix.go-version }}-linux-amd64-${{ github.sha }} - name: Move SOPS binary - run: mv sops-linux-amd64-${{ github.sha }} ./functional-tests/sops + run: mv sops-${{ matrix.go-version }}-linux-amd64-${{ github.sha }} ./functional-tests/sops - name: Make SOPS binary executable run: chmod +x ./functional-tests/sops @@ -116,3 +133,22 @@ jobs: - name: Run tests run: cargo test working-directory: ./functional-tests + + # The 'check' job should depend on all other jobs so it's possible to configure branch protection only for 'check' + # instead of having to explicitly list all individual jobs that need to pass. + check: + if: always() + + needs: + - build + - test + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + allowed-failures: docs, linters + allowed-skips: non-voting-flaky-job + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 573f7d3a8..befefe65a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,11 +29,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: languages: go # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs @@ -45,9 +47,11 @@ jobs: # target, which includes a lot more than just the Go files we want to # scan. - name: Build - run: make install + run: | + make vendor + make install - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: category: "/language:go" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0454f05a6..65b7d49aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,9 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.0.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Install rstcheck and markdownlint run: | diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 000000000..d0626a2cb --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,39 @@ +name: Linters + +on: + push: + branches: + - main + pull_request: + branches: + - main + # Only run when Rust version or linted files change + paths: + - 'rust-toolchain.toml' + - 'functional-tests/**/*.rs' + +permissions: + contents: read + +jobs: + lint: + name: Lint Rust source files + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + # Rustup will detect toolchain version and profile from rust-toolchain.toml + # It will download and install the toolchain and components automatically + # and make them available for subsequent commands + - name: Install Rust toolchain and additional components + run: rustup component add rustfmt + + - name: Show Rust version + run: cargo --version + + - name: Run Formatting Check + run: cargo fmt --check + working-directory: ./functional-tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d756b2624..b6cc0edca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,37 +25,38 @@ jobs: steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Setup Go - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4.0.1 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v4.0.1 with: - go-version: 1.21.x + go-version-file: go.mod cache: false - name: Setup Syft - uses: anchore/sbom-action/download-syft@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5 # v0.17.0 + uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 - name: Setup Cosign - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 + uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 - name: Setup QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Setup Docker Buildx - uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Quay.io - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: quay.io username: ${{ secrets.QUAY_BOT_USERNAME }} @@ -63,7 +64,7 @@ jobs: - name: Run GoReleaser id: goreleaser - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 with: # Note that the following is the version of goreleaser, and NOT a Go version! # When bumping it, make sure to check out goreleaser's changelog first! @@ -168,7 +169,7 @@ jobs: id-token: write # For creating OIDC tokens for signing. contents: write # For adding assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: "${{ needs.combine-subjects.outputs.all-subjects }}" upload-assets: true @@ -185,7 +186,7 @@ jobs: strategy: matrix: ${{ fromJSON(needs.release.outputs.container-subjects) }} - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: ghcr.io/${{ matrix.image }} digest: ${{ matrix.digest }} @@ -204,7 +205,7 @@ jobs: strategy: matrix: ${{ fromJSON(needs.release.outputs.container-subjects) }} - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: quay.io/${{ matrix.image }} digest: ${{ matrix.digest }} diff --git a/.gitignore b/.gitignore index eb1f7c2a1..5eb4a860c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ bin/ dist/ functional-tests/sops -Cargo.lock vendor/ profile.out diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b28f8d52c..a4895a61f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -69,6 +69,7 @@ builds: - windows goarch: - amd64 + - arm64 # Modified timestamp on the binary, set to ensure reproducible builds. mod_timestamp: "{{ .CommitTimestamp }}" diff --git a/.release/alpine.Dockerfile b/.release/alpine.Dockerfile index eaffdfd73..94e205694 100644 --- a/.release/alpine.Dockerfile +++ b/.release/alpine.Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.18 +FROM alpine:3.21 RUN apk --no-cache add \ ca-certificates \ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..f27b5904a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,620 @@ +# Changelog + +## 3.9.4 + +Improvements: + +* Dependency updates ([#1727](https://github.com/getsops/sops/pull/1727), [#1732](https://github.com/getsops/sops/pull/1732), + [#1734](https://github.com/getsops/sops/pull/1734), [#1739](https://github.com/getsops/sops/pull/1739)). + +Bugfixes: + +* Prevent key deduplication to identify different AWS KMS keys that only differ by + role, context, or profile ([#1733](https://github.com/getsops/sops/pull/1733)). +* Update part of Azure SDK which prevented decryption in some cases ([#1695](https://github.com/getsops/sops/issue/1695), + [#1734](https://github.com/getsops/sops/pull/1734)). + +Project changes: + +* CI dependency updates ([#1730](https://github.com/getsops/sops/pull/1730), [#1738](https://github.com/getsops/sops/pull/1738)). +* Rust dependency updates ([#1728](https://github.com/getsops/sops/pull/1728), [#1731](https://github.com/getsops/sops/pull/1731), + [#1735](https://github.com/getsops/sops/pull/1735)). + +## 3.9.3 + +Improvements: + +* Dependency updates ([#1699](https://github.com/getsops/sops/pull/1699), [#1703](https://github.com/getsops/sops/pull/1703), + [#1710](https://github.com/getsops/sops/pull/1710), [#1714](https://github.com/getsops/sops/pull/1714), + [#1715](https://github.com/getsops/sops/pull/1715), [#1723](https://github.com/getsops/sops/pull/1723)). +* Add `persist-credentials: false` to checkouts in GitHub workflows ([#1704](https://github.com/getsops/sops/pull/1704)). +* Tests: use container images from + [https://github.com/getsops/ci-container-images](https://github.com/getsops/ci-container-images) + ([#1722](https://github.com/getsops/sops/pull/1722)). + +Bugfixes: + +* GnuPG: do not incorrectly trim fingerprint in presence of exclamation + marks for specfic subkey selection ([#1720](https://github.com/getsops/sops/pull/1720)). +* `updatekeys` subcommand: fix `--input-type` CLI flag being ignored ([#1721](https://github.com/getsops/sops/pull/1721)). + +Project changes: + +* CI dependency updates ([#1698](https://github.com/getsops/sops/pull/1698), [#1708](https://github.com/getsops/sops/pull/1708), + [#1717](https://github.com/getsops/sops/pull/1717)). +* Rust dependency updates ([#1707](https://github.com/getsops/sops/pull/1707), [#1716](https://github.com/getsops/sops/pull/1716), + [#1725](https://github.com/getsops/sops/pull/1725)). + +## 3.9.2 + +Improvements: + +* Dependency updates ([#1645](https://github.com/getsops/sops/pull/1645), [#1649](https://github.com/getsops/sops/pull/1649), + [#1653](https://github.com/getsops/sops/pull/1653), [#1662](https://github.com/getsops/sops/pull/1662), + [#1686](https://github.com/getsops/sops/pull/1686), [#1693](https://github.com/getsops/sops/pull/1693)). +* Update compiled Protobuf definitions ([#1688](https://github.com/getsops/sops/pull/1688)). +* Remove unused variables and simplify conditional (##1687). + +Bugfixes: + +* Handle whitespace in Azure Key Vault URLs ([#1652](https://github.com/getsops/sops/pull/1652)). +* Correctly handle comments during JSON serialization ([#1647](https://github.com/getsops/sops/pull/1647)). + +Project changes: + +* CI dependency updates ([#1644](https://github.com/getsops/sops/pull/1644), [#1648](https://github.com/getsops/sops/pull/1648), + [#1654](https://github.com/getsops/sops/pull/1654), [#1664](https://github.com/getsops/sops/pull/1664), + [#1673](https://github.com/getsops/sops/pull/1673), [#1677](https://github.com/getsops/sops/pull/1677), + [#1685](https://github.com/getsops/sops/pull/1685)). +* Rust dependency updates ([#1655](https://github.com/getsops/sops/pull/1655), [#1663](https://github.com/getsops/sops/pull/1663), + [#1670](https://github.com/getsops/sops/pull/1670), [#1676](https://github.com/getsops/sops/pull/1676), + [#1689](https://github.com/getsops/sops/pull/1689)). +* Update and improve Protobuf code generation ([#1688](https://github.com/getsops/sops/pull/1688)). + +## 3.9.1 + +Improvements: + +* Dependency updates ([#1550](https://github.com/getsops/sops/pull/1550), [#1554](https://github.com/getsops/sops/pull/1554), + [#1558](https://github.com/getsops/sops/pull/1558), [#1562](https://github.com/getsops/sops/pull/1562), + [#1565](https://github.com/getsops/sops/pull/1565), [#1568](https://github.com/getsops/sops/pull/1568), + [#1575](https://github.com/getsops/sops/pull/1575), [#1581](https://github.com/getsops/sops/pull/1581), + [#1589](https://github.com/getsops/sops/pull/1589), [#1593](https://github.com/getsops/sops/pull/1593), + [#1602](https://github.com/getsops/sops/pull/1602), [#1603](https://github.com/getsops/sops/pull/1603), + [#1618](https://github.com/getsops/sops/pull/1618), [#1629](https://github.com/getsops/sops/pull/1629), + [#1635](https://github.com/getsops/sops/pull/1635), [#1639](https://github.com/getsops/sops/pull/1639), + [#1640](https://github.com/getsops/sops/pull/1640)). +* Clarify naming of the configuration file in the documentation ([#1569](https://github.com/getsops/sops/pull/1569)). +* Build with Go 1.22 ([#1589](https://github.com/getsops/sops/pull/1589)). +* Specify filename of missing file in error messages ([#1625](https://github.com/getsops/sops/pull/1625)). +* `updatekeys` subcommand: show changes in `shamir_threshold` ([#1609](https://github.com/getsops/sops/pull/1609)). + +Bugfixes: + +* Fix the URL used for determining the latest SOPS version ([#1553](https://github.com/getsops/sops/pull/1553)). +* `updatekeys` subcommand: actually use option + `--shamir-secret-sharing-threshold` ([#1608](https://github.com/getsops/sops/pull/1608)). +* Fix `--config` being ignored in subcommands by `loadConfig` ([#1613](https://github.com/getsops/sops/pull/1613)). +* Allow `edit` subcommand to create files ([#1596](https://github.com/getsops/sops/pull/1596)). +* Do not encrypt if a key group is empty, or there are no key groups ([#1600](https://github.com/getsops/sops/pull/1600)). +* Do not ignore config errors when trying to parse a config file ([#1614](https://github.com/getsops/sops/pull/1614)). + +Project changes: + +* CI dependency updates ([#1551](https://github.com/getsops/sops/pull/1551), [#1555](https://github.com/getsops/sops/pull/1555), + [#1559](https://github.com/getsops/sops/pull/1559), [#1564](https://github.com/getsops/sops/pull/1564), + [#1566](https://github.com/getsops/sops/pull/1566), [#1574](https://github.com/getsops/sops/pull/1574), + [#1584](https://github.com/getsops/sops/pull/1584), [#1586](https://github.com/getsops/sops/pull/1586), + [#1590](https://github.com/getsops/sops/pull/1590), [#1592](https://github.com/getsops/sops/pull/1592), + [#1619](https://github.com/getsops/sops/pull/1619), [#1628](https://github.com/getsops/sops/pull/1628), + [#1634](https://github.com/getsops/sops/pull/1634)). +* Improve CI workflows ([#1548](https://github.com/getsops/sops/pull/1548), [#1630](https://github.com/getsops/sops/pull/1630)). +* Ignore user-set environment variable `SOPS_AGE_KEY_FILE` in tests ([#1595](https://github.com/getsops/sops/pull/1595)). +* Add example of using Age recipients in `.sops.yaml` ([#1607](https://github.com/getsops/sops/pull/1607)). +* Add linting check for Rust code formatting ([#1604](https://github.com/getsops/sops/pull/1604)). +* Set Rust version globally via `rust-toolchain.toml` for functional tests ([#1612](https://github.com/getsops/sops/pull/1612)). +* Improve test coverage ([#1617](https://github.com/getsops/sops/pull/1617)). +* Improve tests ([#1622](https://github.com/getsops/sops/pull/1622), [#1624](https://github.com/getsops/sops/pull/1624)). +* Simplify branch rules to check DCO and `check` task instead of an explicit + list of tasks in the CLI workflow ([#1621](https://github.com/getsops/sops/pull/1621)). +* Build with Go 1.22 and 1.23 in CI and update Vault to 1.14 ([#1531](https://github.com/getsops/sops/pull/1531)). +* Build release with Go 1.22 ([#1615](https://github.com/getsops/sops/pull/1615)). +* Fix Dependabot config for Docker; add Dependabot config for Rust ([#1632](https://github.com/getsops/sops/pull/1632)). +* Lock Rust package versions for functional tests for improved + reproducibility ([#1637](https://github.com/getsops/sops/pull/1637)). +* Rust dependency updates ([#1638](https://github.com/getsops/sops/pull/1638)). + +## 3.9.0 + +Features: + +* Add `--mac-only-encrypted` to compute MAC only over values which + end up encrypted ([#973](https://github.com/getsops/sops/pull/973)) +* Allow configuration of indentation for YAML and JSON stores ([#1273](https://github.com/getsops/sops/pull/1273), + [#1372](https://github.com/getsops/sops/pull/1372)) +* Introduce a `--pristine` flag to `sops exec-env` ([#912](https://github.com/getsops/sops/pull/912)) +* Allow to pass multiple paths to `sops updatekeys` ([#1274](https://github.com/getsops/sops/pull/1274)) +* Allow to override `fileName` with different value ([#1332](https://github.com/getsops/sops/pull/1332)) +* Sort masterkeys according to `--decryption-order` ([#1345](https://github.com/getsops/sops/pull/1345)) +* Add separate subcommands for encryption, decryption, rotating, editing, + and setting values ([#1391](https://github.com/getsops/sops/pull/1391)) +* Add `filestatus` command ([#545](https://github.com/getsops/sops/pull/545)) +* Add command `unset` ([#1475](https://github.com/getsops/sops/pull/1475)) +* Merge key for key groups and make keys unique ([#1493](https://github.com/getsops/sops/pull/1493)) +* Support using comments to select parts to encrypt ([#974](https://github.com/getsops/sops/pull/974), + [#1392](https://github.com/getsops/sops/pull/1392)) + +Deprecations: + +* Deprecate the `--background` option to `exec-env` and `exec-file` ([#1379](https://github.com/getsops/sops/pull/1379)) + +Improvements: + +* Warn/fail if the wrong number of arguments is provided ([#1342](https://github.com/getsops/sops/pull/1342)) +* Warn if more than one command is used ([#1388](https://github.com/getsops/sops/pull/1388)) +* Dependency updates ([#1327](https://github.com/getsops/sops/pull/1327), + [#1328](https://github.com/getsops/sops/pull/1328), [#1330](https://github.com/getsops/sops/pull/1330), + [#1336](https://github.com/getsops/sops/pull/1336), [#1334](https://github.com/getsops/sops/pull/1334), + [#1344](https://github.com/getsops/sops/pull/1344), [#1348](https://github.com/getsops/sops/pull/1348), + [#1354](https://github.com/getsops/sops/pull/1354), [#1357](https://github.com/getsops/sops/pull/1357), + [#1360](https://github.com/getsops/sops/pull/1360), [#1373](https://github.com/getsops/sops/pull/1373), + [#1381](https://github.com/getsops/sops/pull/1381), [#1383](https://github.com/getsops/sops/pull/1383), + [#1385](https://github.com/getsops/sops/pull/1385), [#1408](https://github.com/getsops/sops/pull/1408), + [#1428](https://github.com/getsops/sops/pull/1428), [#1429](https://github.com/getsops/sops/pull/1429), + [#1427](https://github.com/getsops/sops/pull/1427), [#1439](https://github.com/getsops/sops/pull/1439), + [#1454](https://github.com/getsops/sops/pull/1454), [#1460](https://github.com/getsops/sops/pull/1460), + [#1466](https://github.com/getsops/sops/pull/1466), [#1489](https://github.com/getsops/sops/pull/1489), + [#1519](https://github.com/getsops/sops/pull/1519), [#1525](https://github.com/getsops/sops/pull/1525), + [#1528](https://github.com/getsops/sops/pull/1528), [#1540](https://github.com/getsops/sops/pull/1540), + [#1543](https://github.com/getsops/sops/pull/1543), [#1545](https://github.com/getsops/sops/pull/1545)) +* Build with Go 1.21 ([#1427](https://github.com/getsops/sops/pull/1427)) +* Improve README.rst ([#1339](https://github.com/getsops/sops/pull/1339), + [#1399](https://github.com/getsops/sops/pull/1399), [#1350](https://github.com/getsops/sops/pull/1350)) +* Fix typos ([#1337](https://github.com/getsops/sops/pull/1337), [#1477](https://github.com/getsops/sops/pull/1477), + [#1484](https://github.com/getsops/sops/pull/1484)) +* Polish the `sops help` output a bit ([#1341](https://github.com/getsops/sops/pull/1341), + [#1544](https://github.com/getsops/sops/pull/1544)) +* Improve and fix tests ([#1346](https://github.com/getsops/sops/pull/1346), + [#1349](https://github.com/getsops/sops/pull/1349), [#1370](https://github.com/getsops/sops/pull/1370), + [#1390](https://github.com/getsops/sops/pull/1390), [#1396](https://github.com/getsops/sops/pull/1396), + [#1492](https://github.com/getsops/sops/pull/1492)) +* Create a constant for the `sops` metadata key ([#1398](https://github.com/getsops/sops/pull/1398)) +* Refactoring: move extraction of encryption and rotation options to + separate functions ([#1389](https://github.com/getsops/sops/pull/1389)) + +Bug fixes: + +* Respect `aws_profile` from keygroup config ([#1049](https://github.com/getsops/sops/pull/1049)) +* Fix a bug where not having a config results in a panic ([#1371](https://github.com/getsops/sops/pull/1371)) +* Consolidate Flatten/Unflatten pre/post processing ([#1356](https://github.com/getsops/sops/pull/1356)) +* INI and DotEnv stores: `shamir_threshold` is an integer ([#1394](https://github.com/getsops/sops/pull/1394)) +* Make check whether file contains invalid keys for encryption dependent + on output store ([#1393](https://github.com/getsops/sops/pull/1393)) +* Do not panic if `updatekeys` is used with a config that has no creation + rules defined ([#1506](https://github.com/getsops/sops/pull/1506)) +* `exec-file`: if `--filename` is used, use the provided filename without + random suffix ([#1474](https://github.com/getsops/sops/pull/1474)) +* Do not use DotEnv store for `exec-env`, but specialized environment + serializing code ([#1436](https://github.com/getsops/sops/pull/1436)) +* Decryption: do not fail if no matching `creation_rule` is present in + config file ([#1434](https://github.com/getsops/sops/pull/1434)) + +Project changes: + +* CI dependency updates ([#1347](https://github.com/getsops/sops/pull/1347), + [#1359](https://github.com/getsops/sops/pull/1359), [#1376](https://github.com/getsops/sops/pull/1376), + [#1382](https://github.com/getsops/sops/pull/1382), [#1386](https://github.com/getsops/sops/pull/1386), + [#1425](https://github.com/getsops/sops/pull/1425), [#1432](https://github.com/getsops/sops/pull/1432), + [#1498](https://github.com/getsops/sops/pull/1498), [#1503](https://github.com/getsops/sops/pull/1503), + [#1508](https://github.com/getsops/sops/pull/1508), [#1510](https://github.com/getsops/sops/pull/1510), + [#1516](https://github.com/getsops/sops/pull/1516), [#1521](https://github.com/getsops/sops/pull/1521), + [#1492](https://github.com/getsops/sops/pull/1492), [#1534](https://github.com/getsops/sops/pull/1534)) +* Adjust Makefile to new goreleaser 6.0.0 release ([#1526](https://github.com/getsops/sops/pull/1526)) + +## 3.8.1 + +Improvements: + +* Improve handling of errors when binary store handles bad data ([#1289](https://github.com/getsops/sops/pull/1289)) +* On macOS, prefer `XDG_CONFIG_HOME` over os.UserConfigDir() ([#1291](https://github.com/getsops/sops/pull/1291)) +* Dependency updates ([#1306](https://github.com/getsops/sops/pull/1306), + [#1319](https://github.com/getsops/sops/pull/1319), [#1325](https://github.com/getsops/sops/pull/1325)) +* pgp: better error reporting for missing GPG binary during import of keys ([#1286](https://github.com/getsops/sops/pull/1286)) +* Fix descriptions of `unencrypted-regex` and `encrypted-regex` flags, and + ensure `unencrypted_regex` is considered in config validation ([#1300](https://github.com/getsops/sops/pull/1300)) +* stores/json: improve error messages when parsing invalid JSON ([#1307](https://github.com/getsops/sops/pull/1307)) + +Bug fixes: + +* pgp: improve handling of GnuPG home dir ([#1298](https://github.com/getsops/sops/pull/1298)) +* Do not crash if an empty YAML file is encrypted ([#1290](https://github.com/getsops/sops/pull/1290)) +* Handling of various ignored errors ([#1304](https://github.com/getsops/sops/pull/1304), + [#1311](https://github.com/getsops/sops/pull/1311)) +* pgp: do not require abs path for `SOPS_GPG_EXEC` ([#1309](https://github.com/getsops/sops/pull/1309)) +* Report key rotation errors ([#1317](https://github.com/getsops/sops/pull/1317)) +* Ensure wrapping of errors in main package ([#1318](https://github.com/getsops/sops/pull/1318)) + +Project changes: + +* Enrich AWS authentication documentation ([#1272](https://github.com/getsops/sops/pull/1272)) +* Add linting for RST and MD files ([#1287](https://github.com/getsops/sops/pull/1287)) +* Delete SOPS encrypted file we don't have keys for ([#1288](https://github.com/getsops/sops/pull/1288)) +* CI dependency updates ([#1295](https://github.com/getsops/sops/pull/1295), [#1301](https://github.com/getsops/sops/pull/1301)) +* pgp: make error the last return value ([#1310](https://github.com/getsops/sops/pull/1310)) +* Improve documentation files ([#1320](https://github.com/getsops/sops/pull/1320)) + +## 3.8.0 + +Features: + +* Support `--version` without network requests using `--disable-version-check` ([#1115](https://github.com/getsops/sops/pull/1115)) +* Support `--input-type` for updatekeys command ([#1116](https://github.com/getsops/sops/pull/1116)) + +Improvements: + +* pgp: modernize and improve, and add tests ([#1054](https://github.com/getsops/sops/pull/1054), + [#1282](https://github.com/getsops/sops/pull/1282)) +* azkv: update SDK to latest, add tests, tidy ([#1067](https://github.com/getsops/sops/pull/1067), + [#1092](https://github.com/getsops/sops/pull/1092), [#1256](https://github.com/getsops/sops/pull/1256)) +* age: improve identity loading, add tests, tidy ([#1064](https://github.com/getsops/sops/pull/1064)) +* kms: AWS SDK V2, allow creds config, add tests ([#1065](https://github.com/getsops/sops/pull/1065), + [#1257](https://github.com/getsops/sops/pull/1257)) +* gcpkms: update SDK to latest, add tests, tidy ([#1072](https://github.com/getsops/sops/pull/1072), + [#1255](https://github.com/getsops/sops/pull/1255)) +* hcvault: update API, add tests, tidy ([#1085](https://github.com/getsops/sops/pull/1085)) +* Do not report version when upstream `--version` check fails ([#1124](https://github.com/getsops/sops/pull/1124)) +* Use GitHub endpoints in `--version` command ([#1261](https://github.com/getsops/sops/pull/1261)) +* Close temporary file before invoking editor to widen support on Windows ([#1265](https://github.com/getsops/sops/pull/1265)) +* Update dependencies ([#1063](https://github.com/getsops/sops/pull/1063), + [#1091](https://github.com/getsops/sops/pull/1091), [#1147](https://github.com/getsops/sops/pull/1147), + [#1242](https://github.com/getsops/sops/pull/1242), [#1260](https://github.com/getsops/sops/pull/1260), + [#1264](https://github.com/getsops/sops/pull/1264), [#1275](https://github.com/getsops/sops/pull/1275), + [#1280](https://github.com/getsops/sops/pull/1280), [#1283](https://github.com/getsops/sops/pull/1283)) +* Deal with various deprecations of dependencies ([#1113](https://github.com/getsops/sops/pull/1113), + [#1262](https://github.com/getsops/sops/pull/1262)) + +Bug fixes: + +* Ensure YAML comments are not displaced ([#1069](https://github.com/getsops/sops/pull/1069)) +* Ensure default Google credentials can be used again after introduction + of `GOOGLE_CREDENTIALS` ([#1249](https://github.com/getsops/sops/pull/1249)) +* Avoid duplicate logging of errors in some key sources ([#1146](https://github.com/getsops/sops/pull/1146), + [#1281](https://github.com/getsops/sops/pull/1281)) +* Using `--set` on a root level key does no longer truncate existing values ([#899](https://github.com/getsops/sops/pull/899)) +* Ensure stable order of SOPS parameters in dotenv file ([#1101](https://github.com/getsops/sops/pull/1101)) + +Project changes: + +* Update Go to 1.20 ([#1148](https://github.com/getsops/sops/pull/1148)) +* Update rustc functional tests to v1.70.0 ([#1234](https://github.com/getsops/sops/pull/1234)) +* Remove remaining CircleCI workflow ([#1237](https://github.com/getsops/sops/pull/1237)) +* Run CLI workflow on main ([#1243](https://github.com/getsops/sops/pull/1243)) +* Delete obsolete `validation/` artifact ([#1248](https://github.com/getsops/sops/pull/1248)) +* Rename Go module to `github.com/getsops/sops/v3` ([#1247](https://github.com/getsops/sops/pull/1247)) +* Revamp release automation, including (Cosign) signed container images + and checksums file, SLSA3 provenance and SBOMs ([#1250](https://github.com/getsops/sops/pull/1250)) +* Update various bits of documentation ([#1244](https://github.com/getsops/sops/pull/1244)) +* Add missing `--encrypt` flag from Vault example ([#1060](https://github.com/getsops/sops/pull/1060)) +* Add documentation on how to use age in `.sops.yaml` ([#1192](https://github.com/getsops/sops/pull/1192)) +* Improve Make targets and address various issues ([#1258](https://github.com/getsops/sops/pull/1258)) +* Ensure clean working tree in CI ([#1267](https://github.com/getsops/sops/pull/1267)) +* Fix CHANGELOG.rst formatting ([#1269](https://github.com/getsops/sops/pull/1269)) +* Pin GitHub Actions to full length commit SHA and add CodeQL ([#1276](https://github.com/getsops/sops/pull/1276)) +* Enable Dependabot for Docker, GitHub Actions and Go Mod ([#1277](https://github.com/getsops/sops/pull/1277)) +* Generate versioned `.intoto.jsonl` ([#1278](https://github.com/getsops/sops/pull/1278)) +* Update CI dependencies ([#1279](https://github.com/getsops/sops/pull/1279)) + +## 3.7.3 + +Changes: + +* Upgrade dependencies ([#1024](https://github.com/getsops/sops/pull/1024), [#1045](https://github.com/getsops/sops/pull/1045)) +* Build alpine container in CI ([#1018](https://github.com/getsops/sops/pull/1018), + [#1032](https://github.com/getsops/sops/pull/1032), [#1025](https://github.com/getsops/sops/pull/1025)) +* keyservice: accept KeyServiceServer in LocalClient ([#1035](https://github.com/getsops/sops/pull/1035)) +* Add support for GCP Service Account within `GOOGLE_CREDENTIALS` ([#953](https://github.com/getsops/sops/pull/953)) + +Bug fixes: + +* Upload the correct binary for the linux amd64 build ([#1026](https://github.com/getsops/sops/pull/1026)) +* Fix bug when specifying multiple age recipients ([#966](https://github.com/getsops/sops/pull/966)) +* Allow for empty yaml maps ([#908](https://github.com/getsops/sops/pull/908)) +* Limit AWS role names to 64 characters ([#1037](https://github.com/getsops/sops/pull/1037)) + +## 3.7.2 + +Changes: + +* README updates ([#861](https://github.com/getsops/sops/pull/861), [#860](https://github.com/getsops/sops/pull/860)) +* Various test fixes ([#909](https://github.com/getsops/sops/pull/909), + [#906](https://github.com/getsops/sops/pull/906), [#1008](https://github.com/getsops/sops/pull/1008)) +* Added Linux and Darwin arm64 releases ([#911](https://github.com/getsops/sops/pull/911), + [#891](https://github.com/getsops/sops/pull/891)) +* Upgrade to go v1.17 ([#1012](https://github.com/getsops/sops/pull/1012)) +* Support SOPS_AGE_KEY environment variable ([#1006](https://github.com/getsops/sops/pull/1006)) + +Bug fixes: + +* Make sure comments in yaml files are not duplicated ([#866](https://github.com/getsops/sops/pull/866)) +* Make sure configuration file paths work correctly relative to the + config file in us ([#853](https://github.com/getsops/sops/pull/853)) + +## 3.7.1 + +Changes: + +* Security fix +* Add release workflow ([#843](https://github.com/getsops/sops/pull/843)) +* Fix issue where CI wouldn't run against master ([#848](https://github.com/getsops/sops/pull/848)) +* Trim extra whitespace around age keys ([#846](https://github.com/getsops/sops/pull/846)) + +## 3.7.0 + +Features: + +* Add support for age ([#688](https://github.com/getsops/sops/pull/688)) +* Add filename to exec-file ([#761](https://github.com/getsops/sops/pull/761)) + +Changes: + +* On failed decryption with GPG, return the error returned by GPG to the + sops user ([#762](https://github.com/getsops/sops/pull/762)) +* Use yaml.v3 instead of modified yaml.v2 for handling YAML files ([#791](https://github.com/getsops/sops/pull/791)) +* Update aws-sdk-go to version v1.37.18 ([#823](https://github.com/getsops/sops/pull/823)) + +Project Changes: + +* Switch from TravisCI to Github Actions ([#792](https://github.com/getsops/sops/pull/792)) + +## 3.6.1 + +Features: + +* Add support for --unencrypted-regex ([#715](https://github.com/getsops/sops/pull/715)) + +Changes: + +* Use keys.openpgp.org instead of gpg.mozilla.org ([#732](https://github.com/getsops/sops/pull/732)) +* Upgrade AWS SDK version ([#714](https://github.com/getsops/sops/pull/714)) +* Support --input-type for exec-file ([#699](https://github.com/getsops/sops/pull/699)) + +Bug fixes: + +* Fixes broken Vault tests ([#731](https://github.com/getsops/sops/pull/731)) +* Revert "Add standard newline/quoting behavior to dotenv store" ([#706](https://github.com/getsops/sops/pull/706)) + +## 3.6.0 + +Features: + +* Support for encrypting data through the use of Hashicorp Vault ([#655](https://github.com/getsops/sops/pull/655)) +* `sops publish` now supports `--recursive` flag for publishing all files + in a directory ([#602](https://github.com/getsops/sops/pull/602)) +* `sops publish` now supports `--omit-extensions` flag for omitting the + extension in the destination path ([#602](https://github.com/getsops/sops/pull/602)) +* sops now supports JSON arrays of arrays ([#642](https://github.com/getsops/sops/pull/642)) + +Improvements: + +* Updates and standardization for the dotenv store ([#612](https://github.com/getsops/sops/pull/612), + [#622](https://github.com/getsops/sops/pull/622)) +* Close temp files after using them for edit command ([#685](https://github.com/getsops/sops/pull/685)) + +Bug fixes: + +* AWS SDK usage now correctly resolves the `~/.aws/config` file ([#680](https://github.com/getsops/sops/pull/680)) +* `sops updatekeys` now correctly matches config rules ([#682](https://github.com/getsops/sops/pull/682)) +* `sops updatekeys` now correctly uses the config path cli flag ([#672](https://github.com/getsops/sops/pull/672)) +* Partially empty sops config files don't break the use of sops anymore ([#662](https://github.com/getsops/sops/pull/662)) +* Fix possible infinite loop in PGP's passphrase prompt call ([#690](https://github.com/getsops/sops/pull/690)) + +Project changes: + +* Dockerfile now based off of golang version 1.14 ([#649](https://github.com/getsops/sops/pull/649)) +* Push alpine version of docker image to Dockerhub ([#609](https://github.com/getsops/sops/pull/609)) +* Push major, major.minor, and major.minor.patch tagged docker images to + Dockerhub ([#607](https://github.com/getsops/sops/pull/607)) +* Removed out of date contact information ([#668](https://github.com/getsops/sops/pull/668)) +* Update authors in the cli help text ([#645](https://github.com/getsops/sops/pull/645)) + +## 3.5.0 + +Features: + +* `sops exec-env` and `sops exec-file`, two new commands for utilizing sops + secrets within a temporary file or env vars + +Bug fixes: + +* Sanitize AWS STS session name, as sops creates it based off of the machines hostname +* Fix for `decrypt.Data` to support `.ini` files +* Various package fixes related to switching to Go Modules +* Fixes for Vault-related tests running locally and in CI. + +Project changes: + +* Change to proper use of go modules, changing to primary module name to + `go.mozilla.org/sops/v3` +* Change tags to requiring a `v` prefix. +* Add documentation for `sops updatekeys` command + +## 3.4.0 + +Features: + +* `sops publish`, a new command for publishing sops encrypted secrets to + S3, GCS, or Hashicorp Vault +* Support for multiple Azure authentication mechanisms +* Azure Keyvault support to the sops config file +* `encrypted_regex` option to the sops config file + +Bug fixes: + +* Return non-zero exit code for invalid CLI flags +* Broken path handling for sops editing on Windows +* `go lint/fmt` violations +* Check for pgp fingerprint before slicing it + +Project changes: + +* Build container using golang 1.12 +* Switch to using go modules +* Hashicorp Vault server in Travis CI build +* Mozilla Publice License file to repo +* Replaced expiring test gpg keys + +## 3.3.1 + +Bug fixes: + +* Make sure the pgp key fingerprint is longer than 16 characters before + slicing it. ([#463](https://github.com/getsops/sops/pull/463)) +* Allow for `--set` value to be a string. ([#461](https://github.com/getsops/sops/pull/461)) + +Project changes: + +* Using `develop` as a staging branch to create releases off of. What + is in `master` is now the current stable release. +* Upgrade to using Go 1.12 to build sops +* Updated all vendored packages + +## 3.3.0 + +New features: + +* Multi-document support for YAML files +* Support referencing AWS KMS keys by their alias +* Support for INI files +* Support for AWS CLI profiles +* Comment support in .env files +* Added vi to the list of known editors +* Added a way to specify the GPG key server to use through the + SOPS_GPG_KEYSERVER environment variable + +Bug fixes: + +* Now uses $HOME instead of ~ (which didn't work) to find the GPG home +* Fix panic when vim was not available as an editor, but other + alternative editors were +* Fix issue with AWS KMS Encryption Contexts ([#445](https://github.com/getsops/sops/pull/445)) + with more than one context value failing to decrypt intermittently. + Includes an automatic fix for old files affected by this issue. + +Project infrastructure changes: + +* Added integration tests for AWS KMS +* Added Code of Conduct + +## 3.2.0 + +* Added --output flag to write output a file directly instead of + through stdout +* Added support for dotenv files + +## 3.1.1 + +* Fix incorrect version number from previous release + +## 3.1.0 + +* Add support for Azure Key Service + +* Fix bug that prevented JSON escapes in input files from working + +## 3.0.5 + +* Prevent files from being encrypted twice + +* Fix empty comments not being decrypted correctly + +* If keyservicecmd returns an error, log it. + +* Initial sops workspace auditing support (still wip) + +* Refactor Store interface to reflect operations SOPS performs + +## 3.0.3 + +* --set now works with nested data structures and not just simple + values + +* Changed default log level to warn instead of info + +* Avoid creating empty files when using the editor mode to create new + files and not making any changes to the example files + +* Output unformatted strings when using --extract instead of encoding + them to yaml + +* Allow forcing binary input and output types from command line flags + +* Deprecate filename_regex in favor of path_regex. filename_regex had + a bug and matched on the whole file path, when it should have only + matched on the file name. path_regex on the other hand is documented + to match on the whole file path. + +* Add an encrypted-suffix option, the exact opposite of + unencrypted-suffix + +* Allow specifying unencrypted_suffix and encrypted_suffix rules in + the .sops.yaml configuration file + +* Introduce key service flag optionally prompting users on + encryption/decryption + +## 3.0.1 + +* Don't consider io.EOF returned by Decoder.Token as error + +* add IsBinary: true to FileHints when encoding with crypto/openpgp + +* some improvements to error messages + +## 3.0.0 + +* Shamir secret sharing scheme support allows SOPS to require multiple master + keys to access a data key and decrypt a file. See `sops groups -help` and the + documentation in README. + +* Keyservice to forward access to a local master key on a socket, similar to + gpg-agent. See `sops keyservice --help` and the documentation in README. + +* Encrypt comments by default + +* Support for Google Compute Platform KMS + +* Refactor of the store logic to separate the internal representation SOPS + has of files from the external representation used in JSON and YAML files + +* Reencoding of versions as string on sops 1.X files. + **WARNING** this change breaks backward compatibility. + SOPS shows an error message with instructions on how to solve + this if it happens. + +* Added command to reconfigure the keys used to encrypt/decrypt a file based on + the `.sops.yaml` config file + +* Retrieve missing PGP keys from gpg.mozilla.org + +* Improved error messages for errors when decrypting files + +## 2.0.0 + +* [major] rewrite in Go + +## 1.14 + +* [medium] Support AWS KMS Encryption Contexts +* [minor] Support insertion in encrypted documents via --set +* [minor] Read location of gpg binary from SOPS_GPG_EXEC env variables + +## 1.13 + +* [minor] handle $EDITOR variable with parameters + +## 1.12 + +* [minor] make sure filename_regex gets applied to file names, not paths +* [minor] move check of latest version under the -V flag +* [medium] fix handling of binary data to preserve file integrity +* [minor] try to use configuration when encrypting existing files diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 64fa3c44f..e0f00d901 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,449 +1,4 @@ Changelog ========= -3.9.0 ------ -Features: - -* Add ``--mac-only-encrypted`` to compute MAC only over values which end up encrypted (#973) -* Allow configuration of indentation for YAML and JSON stores (#1273, #1372) -* Introduce a ``--pristine`` flag to ``sops exec-env`` (#912) -* Allow to pass multiple paths to ``sops updatekeys`` (#1274) -* Allow to override ``fileName`` with different value (#1332) -* Sort masterkeys according to ``--decryption-order`` (#1345) -* Add separate subcommands for encryption, decryption, rotating, editing, and setting values (#1391) -* Add ``filestatus`` command (#545) -* Add command ``unset`` (#1475) -* Merge key for key groups and make keys unique (#1493) -* Support using comments to select parts to encrypt (#974, #1392) - -Deprecations: - -* Deprecate the ``--background`` option to ``exec-env`` and ``exec-file`` (#1379) - -Improvements: - -* Warn/fail if the wrong number of arguments is provided (#1342) -* Warn if more than one command is used (#1388) -* Dependency updates (#1327, #1328, #1330, #1336, #1334, #1344, #1348, #1354, #1357, #1360, #1373, #1381, #1383, #1385, #1408, #1428, #1429, #1427, #1439, #1454, #1460, #1466, #1489, #1519, #1525, #1528, #1540, #1543, #1545) -* Build with Go 1.21 (#1427) -* Improve README.rst (#1339, #1399, #1350) -* Fix typos (#1337, #1477, #1484) -* Polish the ``sops help`` output a bit (#1341, #1544) -* Improve and fix tests (#1346, #1349, #1370, #1390, #1396, #1492) -* Create a constant for the ``sops`` metadata key (#1398) -* Refactoring: move extraction of encryption and rotation options to separate functions (#1389) - -Bug fixes: - -* Respect ``aws_profile`` from keygroup config (#1049) -* Fix a bug where not having a config results in a panic (#1371) -* Consolidate Flatten/Unflatten pre/post processing (#1356) -* INI and DotEnv stores: ``shamir_threshold`` is an integer (#1394) -* Make check whether file contains invalid keys for encryption dependent on output store (#1393) -* Do not panic if ``updatekeys`` is used with a config that has no creation rules defined (#1506) -* ``exec-file``: if ``--filename`` is used, use the provided filename without random suffix (#1474) -* Do not use DotEnv store for ``exec-env``, but specialized environment serializing code (#1436) -* Decryption: do not fail if no matching ``creation_rule`` is present in config file (#1434) - -Project changes: - -* CI dependency updates (#1347, #1359, #1376, #1382, #1386, #1425, #1432, #1498, #1503, #1508, #1510, #1516, #1521, #1492, #1534) -* Adjust Makefile to new goreleaser 6.0.0 release (#1526) - -3.8.1 ------ -Improvements: - -* Improve handling of errors when binary store handles bad data (#1289) -* On macOS, prefer ``XDG_CONFIG_HOME`` over os.UserConfigDir() (#1291) -* Dependency updates (#1306, #1319, #1325) -* pgp: better error reporting for missing GPG binary during import of keys (#1286) -* Fix descriptions of unencrypted-regex and encrypted-regex flags, and ensure unencrypted_regex is considered in config validation (#1300) -* stores/json: improve error messages when parsing invalid JSON (#1307) - -Bug fixes: - -* pgp: improve handling of GnuPG home dir (#1298) -* Do not crash if an empty YAML file is encrypted (#1290) -* Handling of various ignored errors (#1304, #1311) -* pgp: do not require abs path for ``SOPS_GPG_EXEC`` (#1309) -* Report key rotation errors (#1317) -* Ensure wrapping of errors in main package (#1318) - -Project changes: - -* Enrich AWS authentication documentation (#1272) -* Add linting for RST and MD files (#1287) -* Delete SOPS encrypted file we don't have keys for (#1288) -* CI dependency updates (#1295, #1301) -* pgp: make error the last return value (#1310) -* Improve documentation files (#1320) - -3.8.0 ------ -Features: - -* Support ``--version`` without network requests using ``--disable-version-check`` (#1115) -* Support ``--input-type`` for updatekeys command (#1116) - -Improvements: - -* pgp: modernize and improve, and add tests (#1054, #1282) -* azkv: update SDK to latest, add tests, tidy (#1067, #1092, #1256) -* age: improve identity loading, add tests, tidy (#1064) -* kms: AWS SDK V2, allow creds config, add tests (#1065, #1257) -* gcpkms: update SDK to latest, add tests, tidy (#1072, #1255) -* hcvault: update API, add tests, tidy (#1085) -* Do not report version when upstream ``--version`` check fails (#1124) -* Use GitHub endpoints in ``--version`` command (#1261) -* Close temporary file before invoking editor to widen support on Windows (#1265) -* Update dependencies (#1063, #1091, #1147, #1242, #1260, #1264, #1275, #1280, #1283) -* Deal with various deprecations of dependencies (#1113, #1262) - -Bug fixes: - -* Ensure YAML comments are not displaced (#1069) -* Ensure default Google credentials can be used again after introduction of ``GOOGLE_CREDENTIALS`` (#1249) -* Avoid duplicate logging of errors in some key sources (#1146, #1281) -* Using ``--set`` on a root level key does no longer truncate existing values (#899) -* Ensure stable order of SOPS parameters in dotenv file (#1101) - -Project changes: - -* Update Go to 1.20 (#1148) -* Update rustc functional tests to v1.70.0 (#1234) -* Remove remaining CircleCI workflow (#1237) -* Run CLI workflow on main (#1243) -* Delete obsolete ``validation/`` artifact (#1248) -* Rename Go module to ``github.com/getsops/sops/v3`` (#1247) -* Revamp release automation, including (Cosign) signed container images and checksums file, SLSA3 provenance and SBOMs (#1250) -* Update various bits of documentation (#1244) -* Add missing ``--encrypt`` flag from Vault example (#1060) -* Add documentation on how to use age in ``.sops.yaml`` (#1192) -* Improve Make targets and address various issues (#1258) -* Ensure clean working tree in CI (#1267) -* Fix CHANGELOG.rst formatting (#1269) -* Pin GitHub Actions to full length commit SHA and add CodeQL (#1276) -* Enable Dependabot for Docker, GitHub Actions and Go Mod (#1277) -* Generate versioned ``.intoto.jsonl`` (#1278) -* Update CI dependencies (#1279) - -3.7.3 ------ -Changes: - -* Upgrade dependencies (#1024, #1045) -* Build alpine container in CI (#1018, #1032, #1025) -* keyservice: accept KeyServiceServer in LocalClient (#1035) -* Add support for GCP Service Account within ``GOOGLE_CREDENTIALS`` (#953) - -Bug fixes: - -* Upload the correct binary for the linux amd64 build (#1026) -* Fix bug when specifying multiple age recipients (#966) -* Allow for empty yaml maps (#908) -* Limit AWS role names to 64 characters (#1037) - -3.7.2 ------ -Changes: - -* README updates (#861, #860) -* Various test fixes (#909, #906, #1008) -* Added Linux and Darwin arm64 releases (#911, #891) -* Upgrade to go v1.17 (#1012) -* Support SOPS_AGE_KEY environment variable (#1006) - -Bug fixes: - -* Make sure comments in yaml files are not duplicated (#866) -* Make sure configuration file paths work correctly relative to the config file in us (#853) - -3.7.1 ------ -Changes: - -* Security fix -* Add release workflow (#843) -* Fix issue where CI wouldn't run against master (#848) -* Trim extra whitespace around age keys (#846) - -3.7.0 ------ -Features: - -* Add support for age (#688) -* Add filename to exec-file (#761) - -Changes: - -* On failed decryption with GPG, return the error returned by GPG to the sops user (#762) -* Use yaml.v3 instead of modified yaml.v2 for handling YAML files (#791) -* Update aws-sdk-go to version v1.37.18 (#823) - -Project Changes: - -* Switch from TravisCI to Github Actions (#792) - -3.6.1 ------ -Features: - -* Add support for --unencrypted-regex (#715) - -Changes: - -* Use keys.openpgp.org instead of gpg.mozilla.org (#732) -* Upgrade AWS SDK version (#714) -* Support --input-type for exec-file (#699) - -Bug fixes: - -* Fixes broken Vault tests (#731) -* Revert "Add standard newline/quoting behavior to dotenv store" (#706) - - -3.6.0 ------ -Features: - -* Support for encrypting data through the use of Hashicorp Vault (#655) -* ``sops publish`` now supports ``--recursive`` flag for publishing all files in a directory (#602) -* ``sops publish`` now supports ``--omit-extensions`` flag for omitting the extension in the destination path (#602) -* sops now supports JSON arrays of arrays (#642) - -Improvements: - -* Updates and standardization for the dotenv store (#612, #622) -* Close temp files after using them for edit command (#685) - -Bug fixes: - -* AWS SDK usage now correctly resolves the ``~/.aws/config`` file (#680) -* ``sops updatekeys`` now correctly matches config rules (#682) -* ``sops updatekeys`` now correctly uses the config path cli flag (#672) -* Partially empty sops config files don't break the use of sops anymore (#662) -* Fix possible infinite loop in PGP's passphrase prompt call (#690) - -Project changes: - -* Dockerfile now based off of golang version 1.14 (#649) -* Push alpine version of docker image to Dockerhub (#609) -* Push major, major.minor, and major.minor.patch tagged docker images to Dockerhub (#607) -* Removed out of date contact information (#668) -* Update authors in the cli help text (#645) - - -3.5.0 ------ -Features: - -* ``sops exec-env`` and ``sops exec-file``, two new commands for utilizing sops secrets within a temporary file or env vars - -Bug fixes: - -* Sanitize AWS STS session name, as sops creates it based off of the machines hostname -* Fix for ``decrypt.Data`` to support ``.ini`` files -* Various package fixes related to switching to Go Modules -* Fixes for Vault-related tests running locally and in CI. - -Project changes: - -* Change to proper use of go modules, changing to primary module name to ``go.mozilla.org/sops/v3`` -* Change tags to requiring a ``v`` prefix. -* Add documentation for ``sops updatekeys`` command - -3.4.0 ------ -Features: - -* ``sops publish``, a new command for publishing sops encrypted secrets to S3, GCS, or Hashicorp Vault -* Support for multiple Azure authentication mechanisms -* Azure Keyvault support to the sops config file -* ``encrypted_regex`` option to the sops config file - -Bug fixes: - -* Return non-zero exit code for invalid CLI flags -* Broken path handling for sops editing on Windows -* ``go lint/fmt`` violations -* Check for pgp fingerprint before slicing it - -Project changes: - -* Build container using golang 1.12 -* Switch to using go modules -* Hashicorp Vault server in Travis CI build -* Mozilla Publice License file to repo -* Replaced expiring test gpg keys - -3.3.1 ------ - -Bug fixes: - -* Make sure the pgp key fingerprint is longer than 16 characters before - slicing it. (#463) -* Allow for ``--set`` value to be a string. (#461) - -Project changes: - -* Using ``develop`` as a staging branch to create releases off of. What - is in ``master`` is now the current stable release. -* Upgrade to using Go 1.12 to build sops -* Updated all vendored packages - -3.3.0 ------ - -New features: - -* Multi-document support for YAML files -* Support referencing AWS KMS keys by their alias -* Support for INI files -* Support for AWS CLI profiles -* Comment support in .env files -* Added vi to the list of known editors -* Added a way to specify the GPG key server to use through the - SOPS_GPG_KEYSERVER environment variable - -Bug fixes: - -* Now uses $HOME instead of ~ (which didn't work) to find the GPG home -* Fix panic when vim was not available as an editor, but other - alternative editors were -* Fix issue with AWS KMS Encryption Contexts (#445) with more than one - context value failing to decrypt intermittently. Includes an - automatic fix for old files affected by this issue. - -Project infrastructure changes: - -* Added integration tests for AWS KMS -* Added Code of Conduct - - -3.2.0 ------ - -* Added --output flag to write output a file directly instead of - through stdout -* Added support for dotenv files - -3.1.1 ------ - -* Fix incorrect version number from previous release - -3.1.0 ------ - -* Add support for Azure Key Service - -* Fix bug that prevented JSON escapes in input files from working - -3.0.5 ------ - -* Prevent files from being encrypted twice - -* Fix empty comments not being decrypted correctly - -* If keyservicecmd returns an error, log it. - -* Initial sops workspace auditing support (still wip) - -* Refactor Store interface to reflect operations SOPS performs - -3.0.3 ------ - -* --set now works with nested data structures and not just simple - values - -* Changed default log level to warn instead of info - -* Avoid creating empty files when using the editor mode to create new - files and not making any changes to the example files - -* Output unformatted strings when using --extract instead of encoding - them to yaml - -* Allow forcing binary input and output types from command line flags - -* Deprecate filename_regex in favor of path_regex. filename_regex had - a bug and matched on the whole file path, when it should have only - matched on the file name. path_regex on the other hand is documented - to match on the whole file path. - -* Add an encrypted-suffix option, the exact opposite of - unencrypted-suffix - -* Allow specifying unencrypted_suffix and encrypted_suffix rules in - the .sops.yaml configuration file - -* Introduce key service flag optionally prompting users on - encryption/decryption - -3.0.1 ------ - -* Don't consider io.EOF returned by Decoder.Token as error - -* add IsBinary: true to FileHints when encoding with crypto/openpgp - -* some improvements to error messages - -3.0.0 ------ - -* Shamir secret sharing scheme support allows SOPS to require multiple master - keys to access a data key and decrypt a file. See ``sops groups -help`` and the - documentation in README. - -* Keyservice to forward access to a local master key on a socket, similar to - gpg-agent. See ``sops keyservice --help`` and the documentation in README. - -* Encrypt comments by default - -* Support for Google Compute Platform KMS - -* Refactor of the store logic to separate the internal representation SOPS - has of files from the external representation used in JSON and YAML files - -* Reencoding of versions as string on sops 1.X files. - **WARNING** this change breaks backward compatibility. - SOPS shows an error message with instructions on how to solve - this if it happens. - -* Added command to reconfigure the keys used to encrypt/decrypt a file based on the .sops.yaml config file - -* Retrieve missing PGP keys from gpg.mozilla.org - -* Improved error messages for errors when decrypting files - - -2.0.0 ------ - -* [major] rewrite in Go - -1.14 ----- - -* [medium] Support AWS KMS Encryption Contexts -* [minor] Support insertion in encrypted documents via --set -* [minor] Read location of gpg binary from SOPS_GPG_EXEC env variables - -1.13 ----- - -* [minor] handle $EDITOR variable with parameters - -1.12 ----- - -* [minor] make sure filename_regex gets applied to file names, not paths -* [minor] move check of latest version under the -V flag -* [medium] fix handling of binary data to preserve file integrity -* [minor] try to use configuration when encrypting existing files +The changelog can be found in `CHANGELOG.md `_. diff --git a/Makefile b/Makefile index 545ac4d8e..e0fbd3cbb 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,12 @@ SYFT_VERSION ?= v0.87.0 GORELEASER := $(BIN_DIR)/goreleaser GORELEASER_VERSION ?= v1.20.0 +PROTOC_GO := $(BIN_DIR)/protoc-gen-go +PROTOC_GO_VERSION ?= v1.35.2 + +PROTOC_GO_GRPC := $(BIN_DIR)/protoc-gen-go-grpc +PROTOC_GO_GRPC_VERSION ?= v1.5.1 + RSTCHECK := $(shell command -v rstcheck) MARKDOWNLINT := $(shell command -v mdl) @@ -67,18 +73,18 @@ checkmd: $(MD_FILES) .PHONY: test test: vendor gpg --import pgp/sops_functional_tests_key.asc 2>&1 1>/dev/null || exit 0 - LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... + unset SOPS_AGE_KEY_FILE; unset SOPS_AGE_KEY_CMD; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./... .PHONY: showcoverage showcoverage: test $(GO) tool cover -html=profile.out .PHONY: generate -generate: keyservice/keyservice.pb.go +generate: install-protoc-go install-protoc-go-grpc keyservice/keyservice.pb.go $(GO) generate %.pb.go: %.proto - protoc --go_out=plugins=grpc:. $< + protoc --plugin gen-go=$(PROTOC_GO) --plugin gen-go-grpc=$(PLUGIN_GO_GRPC) --go-grpc_opt=require_unimplemented_servers=false --go-grpc_out=. --go_out=. $< .PHONY: functional-tests functional-tests: @@ -112,6 +118,14 @@ install-goreleaser: install-syft: $(call go-install-tool,$(SYFT),github.com/anchore/syft/cmd/syft@$(SYFT_VERSION),$(SYFT_VERSION)) +.PHONY: install-protoc-go +install-protoc-go: + $(call go-install-tool,$(PROTOC_GO),google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GO_VERSION),$(PROTOC_GO_VERSION)) + +.PHONY: install-protoc-go-grpc +install-protoc-go-grpc: + $(call go-install-tool,$(PROTOC_GO_GRPC),google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GO_GRPC_VERSION),$(PROTOC_GO_GRPC_VERSION)) + # go-install-tool will 'go install' any package $2 and install it to $1. define go-install-tool @[ -f $(1)-$(3) ] || { \ diff --git a/README.rst b/README.rst index 0c8e4bab4..e930bf229 100644 --- a/README.rst +++ b/README.rst @@ -106,7 +106,8 @@ encryption/decryption transparently and open the cleartext file in an editor please wait while an encryption key is being generated and stored in a secure fashion file written to mynewtestfile.yaml -Editing will happen in whatever ``$EDITOR`` is set to, or, if it's not set, in vim. +Editing will happen in whatever ``$SOPS_EDITOR`` or ``$EDITOR`` is set to, or, if it's +not set, in vim, nano, or vi. Keep in mind that SOPS will wait for the editor to exit, and then try to reencrypt the file. Some GUI editors (atom, sublime) spawn a child process and then exit immediately. They usually have an option to wait for the main editor window to be @@ -188,6 +189,22 @@ the example files and pgp key provided with the repository:: This last step will decrypt ``example.yaml`` using the test private key. +Encrypting with GnuPG subkeys +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to encrypt with specific GnuPG subkeys, it does not suffice to provide the +exact key ID of the subkey to SOPS, since GnuPG might use *another* subkey instead +to encrypt the file key with. To force GnuPG to use a specific subkey, you need to +append ``!`` to the key's fingerprint. + +.. code:: yaml + + creation_rules: + - pgp: >- + 85D77543B3D624B63CEA9E6DBC17301B491B3F21!, + E60892BB9BD89A69F759A1A0A3D652173B763E8F! + +Please note that this is only passed on correctly to GnuPG since SOPS 3.9.3. Encrypting using age ~~~~~~~~~~~~~~~~~~~~ @@ -210,15 +227,43 @@ On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``. Windows, this would be ``%AppData%\sops\age\keys.txt``. You can specify the location of this file manually by setting the environment variable **SOPS_AGE_KEY_FILE**. Alternatively, you can provide the key(s) directly by setting the **SOPS_AGE_KEY** -environment variable. +environment variable. Alternatively, you can provide a command to output the age keys +by setting the **SOPS_AGE_KEY_CMD** environment variable. The contents of this key file should be a list of age X25519 identities, one per line. Lines beginning with ``#`` are considered comments and ignored. Each identity will be tried in sequence until one is able to decrypt the data. -Encrypting with SSH keys via age is not yet supported by SOPS. +Encrypting with SSH keys via age is also supported by SOPS. You can use SSH public keys +("ssh-ed25519 AAAA...", "ssh-rsa AAAA...") as age recipients when encrypting a file. +When decrypting a file, SOPS will look for ``~/.ssh/id_ed25519`` and falls back to +``~/.ssh/id_rsa``. You can specify the location of the private key manually by setting +the environment variableuse **SOPS_AGE_SSH_PRIVATE_KEY_FILE**. +Note that only ``ssh-rsa`` and ``ssh-ed25519`` are supported. +A list of age recipients can be added to the ``.sops.yaml``: + +.. code:: yaml + + creation_rules: + - age: >- + age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c, + age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw + +It is also possible to use ``updatekeys``, when adding or removing age recipients. For example: + +.. code:: sh + + $ sops updatekeys secret.enc.yaml + 2022/02/09 16:32:02 Syncing keys for file /iac/solution1/secret.enc.yaml + The following changes will be made to the file's groups: + Group 1 + age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c + +++ age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw + Is this okay? (y/n):y + 2022/02/09 16:32:04 File /iac/solution1/secret.enc.yaml synced with new keys + Encrypting using GCP KMS ~~~~~~~~~~~~~~~~~~~~~~~~ GCP KMS uses `Application Default Credentials @@ -338,33 +383,38 @@ Encrypting and decrypting from other programs When using ``sops`` in scripts or from other programs, there are often situations where you do not want to write encrypted or decrypted data to disk. The best way to avoid this is to pass data to SOPS via stdin, and to let SOPS write data to stdout. By default, the encrypt and decrypt operations write data to stdout already. To pass -data via stdin, you need to pass ``/dev/stdin`` as the input filename. Please note that this only works on -Unix-like operating systems such as macOS and Linux. On Windows, you have to use named pipes. +data via stdin, you need to not provide an input filename. For encryption, you also must provide the +``--filename-override`` option with the file's filename. The filename will be used to determine the input and output +types, and to select the correct creation rule. -To decrypt data, you can simply do: +The simplest way to decrypt data from stdin is as follows: .. code:: sh - $ cat encrypted-data | sops decrypt /dev/stdin > decrypted-data + $ cat encrypted-data | sops decrypt > decrypted-data -To control the input and output format, pass ``--input-type`` and ``--output-type`` as appropriate. By default, -``sops`` determines the input and output format from the provided filename, which is ``/dev/stdin`` here, and -thus will use the binary store which expects JSON input and outputs binary data on decryption. +By default, ``sops`` determines the input and output format from the provided filename. Since in this case, +no filename is provided, ``sops`` will use the binary store which expects JSON input and outputs binary data +on decryption. This is often not what you want. -For example, to decrypt YAML data and obtain the decrypted result as YAML, use: +To avoid this, you can either provide a filename with ``--filename-override``, or explicitly control +the input and output formats by passing ``--input-type`` and ``--output-type`` as appropriate: .. code:: sh - $ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml /dev/stdin > decrypted-data + $ cat encrypted-data | sops decrypt --filename-override filename.yaml > decrypted-data + $ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml > decrypted-data + +In both cases, ``sops`` will assume that the data you provide is in YAML format, and will encode the decrypted +data in YAML as well. The second form allows to use different formats for input and output. To encrypt, it is important to note that SOPS also uses the filename to look up the correct creation rule from -``.sops.yaml``. Likely ``/dev/stdin`` will not match a creation rule, or only match the fallback rule without -``path_regex``, which is usually not what you want. For that, ``sops`` provides the ``--filename-override`` -parameter which allows you to tell SOPS which filename to use to match creation rules: +``.sops.yaml``. Therefore, you must provide the ``--filename-override`` parameter which allows you to tell +SOPS which filename to use to match creation rules: .. code:: sh - $ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml /dev/stdin > encrypted-data + $ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml > encrypted-data SOPS will find a matching creation rule for ``path/filename.sops.yaml`` in ``.sops.yaml`` and use that one to encrypt the data from stdin. This filename will also be used to determine the input and output store. As always, @@ -373,7 +423,7 @@ the input store type can be adjusted by passing ``--input-type``, and the output .. code:: sh - $ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv /dev/stdin > encrypted-data + $ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv > encrypted-data Encrypting using Hashicorp Vault @@ -485,7 +535,7 @@ disabled by supplying the ``-y`` flag. ****************** The ``rotate`` command generates a new data encryption key and reencrypt all values -with the new key. At te same time, the command line flag ``--add-kms``, ``--add-pgp``, +with the new key. At the same time, the command line flag ``--add-kms``, ``--add-pgp``, ``--add-gcp-kms``, ``--add-azure-kv``, ``--rm-kms``, ``--rm-pgp``, ``--rm-gcp-kms`` and ``--rm-azure-kv`` can be used to add and remove keys from a file. These flags use the comma separated syntax as the ``--kms``, ``--pgp``, ``--gcp-kms`` and ``--azure-kv`` @@ -669,6 +719,11 @@ of all new files. If your secrets are stored under a specific directory, like a ``git`` repository, you can create a ``.sops.yaml`` configuration file at the root directory to define which keys are used for which filename. +.. note:: + + The file needs to be named ``.sops.yaml``. Other names (i.e. ``.sops.yml``) won't be automatically + discovered by SOPS. You'll need to pass the ``--config .sops.yml`` option for it to be picked up. + Let's take an example: * file named **something.dev.yaml** should use one set of KMS A, PGP and age @@ -1026,6 +1081,11 @@ written to disk. $ echo your password: $database_password your password: +If you want process signals to be sent to the command, for example if you are +running ``exec-env`` to launch a server and your server handles SIGTERM, then the +``--same-process`` flag can be used to instruct ``sops`` to start your command in +the same process instead of a child process. This uses the ``execve`` system call +and is supported on Unix-like systems. If the command you want to run only operates on files, you can use ``exec-file`` instead. By default, SOPS will use a FIFO to pass the contents of the @@ -1211,7 +1271,7 @@ When operating on stdin, use the ``--input-type`` and ``--output-type`` flags as .. code:: sh - $ cat myfile.json | sops decrypt --input-type json --output-type json /dev/stdin + $ cat myfile.json | sops decrypt --input-type json --output-type json JSON and JSON_binary indentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/aes/cipher.go b/aes/cipher.go index 291f2fedf..e1009f2a5 100644 --- a/aes/cipher.go +++ b/aes/cipher.go @@ -11,6 +11,7 @@ import ( "fmt" "regexp" "strconv" + "time" "github.com/getsops/sops/v3" "github.com/getsops/sops/v3/logging" @@ -110,6 +111,10 @@ func (c Cipher) Decrypt(ciphertext string, key []byte, additionalData string) (p plaintext = decryptedBytes case "bool": plaintext, err = strconv.ParseBool(decryptedValue) + case "time": + var value time.Time + err = value.UnmarshalText(decryptedBytes) + plaintext = value case "comment": plaintext = sops.Comment{Value: decryptedValue} default: @@ -176,6 +181,12 @@ func (c Cipher) Encrypt(plaintext interface{}, key []byte, additionalData string } else { plainBytes = []byte("False") } + case time.Time: + encryptedType = "time" + plainBytes, err = value.MarshalText() + if err != nil { + return "", fmt.Errorf("Error marshaling timestamp %q: %w", value, err) + } case sops.Comment: encryptedType = "comment" plainBytes = []byte(value.Value) diff --git a/aes/cipher_test.go b/aes/cipher_test.go index 4d53510aa..2c2421faf 100644 --- a/aes/cipher_test.go +++ b/aes/cipher_test.go @@ -1,13 +1,16 @@ package aes import ( + "bytes" "crypto/rand" + "reflect" "strings" "testing" "testing/quick" + "time" - "github.com/stretchr/testify/assert" "github.com/getsops/sops/v3" + "github.com/stretchr/testify/assert" ) func TestDecrypt(t *testing.T) { @@ -108,6 +111,36 @@ func TestRoundtripBool(t *testing.T) { } } +func TestRoundtripTime(t *testing.T) { + key := []byte(strings.Repeat("f", 32)) + parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + assert.Nil(t, err) + loc := time.FixedZone("", 12300) // offset must be divisible by 60, otherwise won't survive a round-trip + values := []time.Time{ + time.UnixMilli(0).In(time.UTC), + time.UnixMilli(123456).In(time.UTC), + time.UnixMilli(123456).In(loc), + time.UnixMilli(123456789).In(time.UTC), + time.UnixMilli(123456789).In(loc), + time.UnixMilli(1234567890).In(time.UTC), + time.UnixMilli(1234567890).In(loc), + parsedTime, + } + for _, value := range values { + s, err := NewCipher().Encrypt(value, key, "foo") + assert.Nil(t, err) + if err != nil { + continue + } + d, err := NewCipher().Decrypt(s, key, "foo") + assert.Nil(t, err) + if err != nil { + continue + } + assert.Equal(t, value, d) + } +} + func TestEncryptEmptyComment(t *testing.T) { key := []byte(strings.Repeat("f", 32)) s, err := NewCipher().Encrypt(sops.Comment{}, key, "") @@ -121,3 +154,61 @@ func TestDecryptEmptyValue(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "", s) } + +// This test would belong more in sops_test.go, but from there we cannot access +// the aes package to get a cipher which can actually handle time.Time objects. +func TestTimestamps(t *testing.T) { + unixTime := time.UnixMilli(123456789).In(time.UTC) + parsedTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + assert.Nil(t, err) + branches := sops.TreeBranches{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: unixTime, + }, + sops.TreeItem{ + Key: "bar", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: parsedTime, + }, + }, + }, + }, + } + tree := sops.Tree{Branches: branches, Metadata: sops.Metadata{UnencryptedSuffix: "_unencrypted"}} + expected := sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: unixTime, + }, + sops.TreeItem{ + Key: "bar", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: parsedTime, + }, + }, + }, + } + cipher := NewCipher() + _, err = tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Encrypting the tree failed: %s", err) + } + if reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees do match: \ngot \t\t%+v,\n not expected \t\t%+v", tree.Branches[0], expected) + } + _, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Decrypting the tree failed: %s", err) + } + assert.Equal(t, tree.Branches[0][0].Value, unixTime) + assert.Equal(t, tree.Branches[0], expected) + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected) + } +} diff --git a/age/encrypted_keys.go b/age/encrypted_keys.go new file mode 100644 index 000000000..c7789db90 --- /dev/null +++ b/age/encrypted_keys.go @@ -0,0 +1,190 @@ +// These functions have been copied from the age project +// https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/encrypted_keys.go +// https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/parse.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + + "filippo.io/age" + "filippo.io/age/armor" + + gpgagent "github.com/getsops/gopgagent" +) + +type EncryptedIdentity struct { + Contents []byte + Passphrase func() (string, error) + NoMatchWarning func() + IncorrectPassphrase func() + + identities []age.Identity +} + +var _ age.Identity = &EncryptedIdentity{} + +func (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { + if i.identities == nil { + if err := i.decrypt(); err != nil { + return nil, err + } + } + + for _, id := range i.identities { + fileKey, err = id.Unwrap(stanzas) + if errors.Is(err, age.ErrIncorrectIdentity) { + continue + } + if err != nil { + return nil, err + } + return fileKey, nil + } + i.NoMatchWarning() + return nil, age.ErrIncorrectIdentity +} + +func (i *EncryptedIdentity) decrypt() error { + d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase}) + if e := new(age.NoIdentityMatchError); errors.As(err, &e) { + // ScryptIdentity returns ErrIncorrectIdentity for an incorrect + // passphrase, which would lead Decrypt to returning "no identity + // matched any recipient". That makes sense in the API, where there + // might be multiple configured ScryptIdentity. Since in cmd/age there + // can be only one, return a better error message. + i.IncorrectPassphrase() + return fmt.Errorf("incorrect passphrase") + } + if err != nil { + return fmt.Errorf("failed to decrypt identity file: %v", err) + } + i.identities, err = age.ParseIdentities(d) + return err +} + +// LazyScryptIdentity is an age.Identity that requests a passphrase only if it +// encounters an scrypt stanza. After obtaining a passphrase, it delegates to +// ScryptIdentity. +type LazyScryptIdentity struct { + Passphrase func() (string, error) +} + +var _ age.Identity = &LazyScryptIdentity{} + +func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { + for _, s := range stanzas { + if s.Type == "scrypt" && len(stanzas) != 1 { + return nil, errors.New("an scrypt recipient must be the only one") + } + } + if len(stanzas) != 1 || stanzas[0].Type != "scrypt" { + return nil, age.ErrIncorrectIdentity + } + pass, err := i.Passphrase() + if err != nil { + return nil, fmt.Errorf("could not read passphrase: %v", err) + } + ii, err := age.NewScryptIdentity(pass) + if err != nil { + return nil, err + } + fileKey, err = ii.Unwrap(stanzas) + return fileKey, err +} + +func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) { + b := bufio.NewReader(reader) + p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE" + peeked := string(p) + + switch { + // An age encrypted file, plain or armored. + case peeked == "age-encryption" || peeked == "-----BEGIN AGE": + var r io.Reader = b + if peeked == "-----BEGIN AGE" { + r = armor.NewReader(r) + } + const privateKeySizeLimit = 1 << 24 // 16 MiB + contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit)) + if err != nil { + return nil, fmt.Errorf("failed to read '%s': %w", key, err) + } + if len(contents) == privateKeySizeLimit { + return nil, fmt.Errorf("failed to read '%s': file too long", key) + } + IncorrectPassphrase := func() { + conn, err := gpgagent.NewConn() + if err != nil { + return + } + defer func(conn *gpgagent.Conn) { + if err := conn.Close(); err != nil { + log.Errorf("failed to close connection with gpg-agent: %s", err) + } + }(conn) + err = conn.RemoveFromCache(key) + if err != nil { + log.Warnf("gpg-agent remove cache request errored: %s", err) + return + } + } + ids := []age.Identity{&EncryptedIdentity{ + Contents: contents, + Passphrase: func() (string, error) { + conn, err := gpgagent.NewConn() + if err != nil { + passphrase, err := readSecret("Enter passphrase for identity " + key + ":") + if err != nil { + return "", err + } + return string(passphrase), nil + } + defer func(conn *gpgagent.Conn) { + if err := conn.Close(); err != nil { + log.Errorf("failed to close connection with gpg-agent: %s", err) + } + }(conn) + + req := gpgagent.PassphraseRequest{ + // TODO is the cachekey good enough? + CacheKey: key, + Prompt: "Passphrase", + Desc: fmt.Sprintf("Enter passphrase for identity '%s':", key), + } + pass, err := conn.GetPassphrase(&req) + if err != nil { + return "", fmt.Errorf("gpg-agent passphrase request errored: %s", err) + } + //make sure that we won't store empty pass + if len(pass) == 0 { + IncorrectPassphrase() + } + return pass, nil + }, + IncorrectPassphrase: IncorrectPassphrase, + NoMatchWarning: func() { + log.Warnf("encrypted identity '%s' didn't match file's recipients", key) + }, + }} + return ids, nil + // An unencrypted age identity file. + default: + ids, err := parseIdentities(b) + if err != nil { + return nil, fmt.Errorf("failed to parse '%s' age identities: %w", key, err) + } + return ids, nil + } +} diff --git a/age/keysource.go b/age/keysource.go index 83bdbe0a6..f77ab738d 100644 --- a/age/keysource.go +++ b/age/keysource.go @@ -1,20 +1,25 @@ package age import ( + "bufio" "bytes" "errors" "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "strings" "filippo.io/age" + "filippo.io/age/agessh" "filippo.io/age/armor" + "filippo.io/age/plugin" "github.com/sirupsen/logrus" "github.com/getsops/sops/v3/logging" + "github.com/google/shlex" ) const ( @@ -24,6 +29,12 @@ const ( // SopsAgeKeyFileEnv can be set as an environment variable pointing to an // age keys file. SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE" + // SopsAgeKeyCmdEnv can be set as an environment variable with a command + // to execute that returns the age keys. + SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD" + // SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to + // a private SSH key file. + SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE" // SopsAgeKeyUserConfigPath is the default age keys file path in // getUserConfigDir(). SopsAgeKeyUserConfigPath = "sops/age/keys.txt" @@ -60,7 +71,7 @@ type MasterKey struct { parsedIdentities []age.Identity // parsedRecipient contains a parsed age public key. // It is used to lazy-load the Recipient at-most once. - parsedRecipient *age.X25519Recipient + parsedRecipient age.Recipient } // MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded @@ -111,7 +122,10 @@ type ParsedIdentities []age.Identity // parsing (using age.ParseIdentities) and appending to the slice yourself, in // combination with e.g. a sync.Mutex. func (i *ParsedIdentities) Import(identity ...string) error { - identities, err := parseIdentities(identity...) + // one identity per line + r := strings.NewReader(strings.Join(identity, "\n")) + + identities, err := parseIdentities(r) if err != nil { return fmt.Errorf("failed to parse and add to age identities: %w", err) } @@ -233,6 +247,35 @@ func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } +// loadAgeSSHIdentity attempts to load the age SSH identity based on an SSH +// private key from the SopsAgeSshPrivateKeyFileEnv environment variable. If the +// environment variable is not present, it will fall back to `~/.ssh/id_ed25519` +// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil. +func loadAgeSSHIdentity() (age.Identity, error) { + sshKeyFilePath, ok := os.LookupEnv(SopsAgeSshPrivateKeyFileEnv) + if ok { + return parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath) + } + + userHomeDir, err := os.UserHomeDir() + if err != nil || userHomeDir == "" { + log.Warnf("could not determine the user home directory: %v", err) + return nil, nil + } + + sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519") + if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil { + return parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath) + } + + sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa") + if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil { + return parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath) + } + + return nil, nil +} + func getUserConfigDir() (string, error) { if runtime.GOOS == "darwin" { if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" { @@ -244,9 +287,19 @@ func getUserConfigDir() (string, error) { // loadIdentities attempts to load the age identities based on runtime // environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv, -// SopsAgeKeyUserConfigPath). It will load all found references, and expects -// at least one configuration to be present. +// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all +// found references, and expects at least one configuration to be present. func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { + var identities ParsedIdentities + + sshIdentity, err := loadAgeSSHIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get SSH identity: %w", err) + } + if sshIdentity != nil { + identities = append(identities, sshIdentity) + } + var readers = make(map[string]io.Reader, 0) if ageKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok { @@ -262,8 +315,20 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { readers[SopsAgeKeyFileEnv] = f } + if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok { + args, err := shlex.Split(ageKeyCmd) + if err != nil { + return nil, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err) + } + out, err := exec.Command(args[0], args[1:]...).Output() + if err != nil { + return nil, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err) + } + readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out) + } + userConfigDir, err := getUserConfigDir() - if err != nil && len(readers) == 0 { + if err != nil && len(readers) == 0 && len(identities) == 0 { return nil, fmt.Errorf("user config directory could not be determined: %w", err) } if userConfigDir != "" { @@ -272,7 +337,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("failed to open file: %w", err) } - if errors.Is(err, os.ErrNotExist) && len(readers) == 0 { + if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 { // If we have no other readers, presence of the file is required. return nil, fmt.Errorf("failed to open file: %w", err) } @@ -282,11 +347,10 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { } } - var identities ParsedIdentities for n, r := range readers { - ids, err := age.ParseIdentities(r) + ids, err := unwrapIdentities(n, r) if err != nil { - return nil, fmt.Errorf("failed to parse '%s' age identities: %w", n, err) + return nil, err } identities = append(identities, ids...) } @@ -294,26 +358,66 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) { } // parseRecipient attempts to parse a string containing an encoded age public -// key. -func parseRecipient(recipient string) (*age.X25519Recipient, error) { - parsedRecipient, err := age.ParseX25519Recipient(recipient) - if err != nil { - return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) +// key or a public ssh key. +func parseRecipient(recipient string) (age.Recipient, error) { + switch { + case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1: + parsedRecipient, err := plugin.NewRecipient(recipient, pluginTerminalUI) + if err != nil { + return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err) + } + return parsedRecipient, nil + case strings.HasPrefix(recipient, "age1"): + parsedRecipient, err := age.ParseX25519Recipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) + } + + return parsedRecipient, nil + case strings.HasPrefix(recipient, "ssh-"): + parsedRecipient, err := agessh.ParseRecipient(recipient) + if err != nil { + return nil, fmt.Errorf("failed to parse input as age-ssh public key: %w", err) + } + return parsedRecipient, nil } - return parsedRecipient, nil + + return nil, fmt.Errorf("failed to parse input, unknown recipient type: %q", recipient) } -// parseIdentities attempts to parse the string set of encoded age identities. -// A single identity argument is allowed to be a multiline string containing -// multiple identities. Empty lines and lines starting with "#" are ignored. -func parseIdentities(identity ...string) (ParsedIdentities, error) { - var identities []age.Identity - for _, i := range identity { - parsed, err := age.ParseIdentities(strings.NewReader(i)) +// parseIdentities attempts to parse one or more age identities from the provided reader. +// One identity per line. +// Empty lines and lines starting with "#" are ignored. +func parseIdentities(r io.Reader) (ParsedIdentities, error) { + var identities ParsedIdentities + + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parsed, err := parseIdentity(line) if err != nil { return nil, err } - identities = append(identities, parsed...) + + identities = append(identities, parsed) } + return identities, nil } + +func parseIdentity(s string) (age.Identity, error) { + switch { + case strings.HasPrefix(s, "AGE-PLUGIN-"): + return plugin.NewIdentity(s, pluginTerminalUI) + case strings.HasPrefix(s, "AGE-SECRET-KEY-1"): + return age.ParseX25519Identity(s) + default: + return nil, fmt.Errorf("unknown identity type") + } +} diff --git a/age/keysource_test.go b/age/keysource_test.go index 1a07058a6..42bdf3c6a 100644 --- a/age/keysource_test.go +++ b/age/keysource_test.go @@ -28,6 +28,35 @@ EylloI7MNGbadPGb -----END AGE ENCRYPTED FILE-----` // mockEncryptedKeyPlain is the plain value of mockEncryptedKey. mockEncryptedKeyPlain string = "data" + // passphrase used to encrypt age identity. + mockIdentityPassphrase string = "passphrase" + mockEncryptedIdentity string = `-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCBMN2FXZW9xSFViYjdNeW5D +dy9iSHFnIDE4Ck9zV0ZoNldmci9rL3VXd3BtZmQvK3VZWEpBQjdhZ0UrcmhqR2lF +YThFMzAKLS0tIGVEQ0xwODI1TlNYeHNHaHZKWHoyLzYwMTMvTGhaZG1oa203cSs0 +VUpBL1kKsaTnt+H/z8mkL21UYKIt3YMpWSV/oYqTm1cSSUnF9InZEYU9HndK9rc8 +ni+MTJCmYf4mgvvGPMf7oIQvs6ijaTdlQb+zeQsL4eif20w+CWgvPNrS6iXUIs8W +w5/fHsxwmrkG96nDkMErJKhmjmLpC+YdbiMe6P/KIpas09m08RTIqcz7ua0Xm3ey +ndU+8ILJOhcnWV55W43nTw/UUFse7f+qY61n7kcd1sGd7ZfSEdEIqS3K2vEtA3ER +fn0s3cyXVEBxL9OZqcAk45bCFVOl13Fp/DBfquHEjvAyeg0= +-----END AGE ENCRYPTED FILE-----` + // mockSshRecipient is a mock age ssh recipient, it matches mockSshIdentity + mockSshRecipient string = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID+Wi8WZw2bXfBpcs/WECttCzP39OkenS6pHWHWGFJvN Test" + // mockSshIdentity is a mock age identity based on an OpenSSH private key (ed25519) + mockSshIdentity string = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQAAAIgCXDMIAlwz +CAAAAAtzc2gtZWQyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQ +AAAEBJdWTJ8dC0OnMcwy4gQ96sp6KG8GE9EiyhFGhKldKiST+Wi8WZw2bXfBpcs/WECttC +zP39OkenS6pHWHWGFJvNAAAABFRlc3QB +-----END OPENSSH PRIVATE KEY-----` + mockEncryptedSshKey string = `-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDJjd0R4dyB2R3Ns +VUNHaXBiTEJaNU5BMFFQZUpCYWJqODFyTTZ4WWZoRVpUd2M2aTBFCkduUFJHb1U2 +K3RqWVQrLzE4anZKZ3h2T3c2MFpZTHlGaHprcElXenByWTAKLS0tIG56MHFSZERl +em9PWmRMMTY4aytYTnVZN04yeER5Z2E3TWxWT3JTZWR2ekUKp/HZLy4MzQqoszGk ++P0hSPPNhOhvFwv4AqCw1+A+WyeHGQPq +-----END AGE ENCRYPTED FILE-----` ) func TestMasterKeysFromRecipients(t *testing.T) { @@ -41,22 +70,32 @@ func TestMasterKeysFromRecipients(t *testing.T) { assert.Equal(t, got[0].Recipient, mockRecipient) }) - t.Run("recipients", func(t *testing.T) { - got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient) + t.Run("recipient-ssh", func(t *testing.T) { + got, err := MasterKeysFromRecipients(mockSshRecipient) assert.NoError(t, err) - assert.Len(t, got, 2) + assert.Len(t, got, 1) + assert.Equal(t, got[0].Recipient, mockSshRecipient) + }) + + t.Run("recipients", func(t *testing.T) { + got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient + "," + mockSshRecipient) + assert.NoError(t, err) + + assert.Len(t, got, 3) assert.Equal(t, got[0].Recipient, mockRecipient) assert.Equal(t, got[1].Recipient, otherRecipient) + assert.Equal(t, got[2].Recipient, mockSshRecipient) }) t.Run("leading and trailing spaces", func(t *testing.T) { - got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " ") + got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " , " + mockSshRecipient + " ") assert.NoError(t, err) - assert.Len(t, got, 2) + assert.Len(t, got, 3) assert.Equal(t, got[0].Recipient, mockRecipient) assert.Equal(t, got[1].Recipient, otherRecipient) + assert.Equal(t, got[2].Recipient, mockSshRecipient) }) t.Run("empty", func(t *testing.T) { @@ -75,6 +114,14 @@ func TestMasterKeyFromRecipient(t *testing.T) { assert.Nil(t, got.parsedIdentities) }) + t.Run("recipient-ssh", func(t *testing.T) { + got, err := MasterKeyFromRecipient(mockSshRecipient) + assert.NoError(t, err) + assert.EqualValues(t, mockSshRecipient, got.Recipient) + assert.NotNil(t, got.parsedRecipient) + assert.Nil(t, got.parsedIdentities) + }) + t.Run("leading and trailing spaces", func(t *testing.T) { got, err := MasterKeyFromRecipient(" " + mockRecipient + " ") assert.NoError(t, err) @@ -83,6 +130,14 @@ func TestMasterKeyFromRecipient(t *testing.T) { assert.Nil(t, got.parsedIdentities) }) + t.Run("leading and trailing spaces - ssh", func(t *testing.T) { + got, err := MasterKeyFromRecipient(" " + mockSshRecipient + " ") + assert.NoError(t, err) + assert.EqualValues(t, mockSshRecipient, got.Recipient) + assert.NotNil(t, got.parsedRecipient) + assert.Nil(t, got.parsedIdentities) + }) + t.Run("invalid recipient", func(t *testing.T) { got, err := MasterKeyFromRecipient("invalid") assert.Error(t, err) @@ -111,6 +166,8 @@ func TestParsedIdentities_ApplyToMasterKey(t *testing.T) { func TestMasterKey_Encrypt(t *testing.T) { mockParsedRecipient, err := parseRecipient(mockRecipient) assert.NoError(t, err) + mockSshParsedRecipient, err := parseRecipient(mockSshRecipient) + assert.NoError(t, err) t.Run("recipient", func(t *testing.T) { key := &MasterKey{ @@ -120,6 +177,14 @@ func TestMasterKey_Encrypt(t *testing.T) { assert.NotEmpty(t, key.EncryptedKey) }) + t.Run("recipient ssh", func(t *testing.T) { + key := &MasterKey{ + Recipient: mockSshRecipient, + } + assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain))) + assert.NotEmpty(t, key.EncryptedKey) + }) + t.Run("parsed recipient", func(t *testing.T) { key := &MasterKey{ parsedRecipient: mockParsedRecipient, @@ -128,13 +193,21 @@ func TestMasterKey_Encrypt(t *testing.T) { assert.NotEmpty(t, key.EncryptedKey) }) + t.Run("parsed recipient ssh", func(t *testing.T) { + key := &MasterKey{ + parsedRecipient: mockSshParsedRecipient, + } + assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain))) + assert.NotEmpty(t, key.EncryptedKey) + }) + t.Run("invalid recipient", func(t *testing.T) { key := &MasterKey{ Recipient: "invalid", } err := key.Encrypt([]byte(mockEncryptedKeyPlain)) assert.Error(t, err) - assert.ErrorContains(t, err, "failed to parse input as Bech32-encoded age public key") + assert.ErrorContains(t, err, "failed to parse input, unknown recipient type:") assert.Empty(t, key.EncryptedKey) }) @@ -180,6 +253,7 @@ func TestMasterKey_Decrypt(t *testing.T) { }) t.Run("loaded identities", func(t *testing.T) { + overwriteUserConfigDir(t, t.TempDir()) key := &MasterKey{EncryptedKey: mockEncryptedKey} t.Setenv(SopsAgeKeyEnv, mockIdentity) @@ -188,6 +262,25 @@ func TestMasterKey_Decrypt(t *testing.T) { assert.EqualValues(t, mockEncryptedKeyPlain, got) }) + t.Run("loaded identities ssh", func(t *testing.T) { + key := &MasterKey{EncryptedKey: mockEncryptedSshKey} + tmp := t.TempDir() + overwriteUserConfigDir(t, tmp) + + homeDir, err := os.UserHomeDir() + assert.NoError(t, err) + keyPath := filepath.Join(homeDir, ".ssh/id_25519") + assert.True(t, strings.HasPrefix(keyPath, homeDir)) + + assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700)) + assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644)) + t.Setenv(SopsAgeSshPrivateKeyFileEnv, keyPath) + + got, err := key.Decrypt() + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + t.Run("no identities", func(t *testing.T) { tmpDir := t.TempDir() overwriteUserConfigDir(t, tmpDir) @@ -215,6 +308,7 @@ func TestMasterKey_Decrypt(t *testing.T) { }) t.Run("invalid encrypted key", func(t *testing.T) { + overwriteUserConfigDir(t, t.TempDir()) key := &MasterKey{EncryptedKey: "invalid"} t.Setenv(SopsAgeKeyEnv, mockIdentity) @@ -327,6 +421,25 @@ func TestMasterKey_loadIdentities(t *testing.T) { assert.Len(t, got, 1) }) + t.Run(SopsAgeSshPrivateKeyFileEnv, func(t *testing.T) { + tmpDir := t.TempDir() + overwriteUserConfigDir(t, tmpDir) + + homeDir, err := os.UserHomeDir() + assert.NoError(t, err) + keyPath := filepath.Join(homeDir, ".ssh/id_25519") + assert.True(t, strings.HasPrefix(keyPath, homeDir)) + + assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700)) + assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644)) + t.Setenv(SopsAgeSshPrivateKeyFileEnv, keyPath) + + key := &MasterKey{} + got, err := key.loadIdentities() + assert.NoError(t, err) + assert.Len(t, got, 1) + }) + t.Run("no identity", func(t *testing.T) { tmpDir := t.TempDir() overwriteUserConfigDir(t, tmpDir) @@ -372,10 +485,37 @@ func TestMasterKey_loadIdentities(t *testing.T) { assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv)) assert.Nil(t, got) }) + + t.Run(SopsAgeKeyCmdEnv, func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "echo '"+mockIdentity+"'") + + key := &MasterKey{} + got, err := key.loadIdentities() + assert.NoError(t, err) + assert.Len(t, got, 1) + }) + + t.Run("cmd error", func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + t.Setenv(SopsAgeKeyCmdEnv, "meow") + + key := &MasterKey{} + got, err := key.loadIdentities() + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to execute command meow") + assert.Nil(t, got) + }) } -// overwriteUserConfigDir sets the user config directory based on the -// os.UserConfigDir logic. +// overwriteUserConfigDir sets the user config directory and the user home directory +// based on the os.UserConfigDir logic. func overwriteUserConfigDir(t *testing.T, path string) { switch runtime.GOOS { case "windows": @@ -384,6 +524,7 @@ func overwriteUserConfigDir(t *testing.T, path string) { t.Setenv("home", path) default: // Unix t.Setenv("XDG_CONFIG_HOME", path) + t.Setenv("HOME", path) } } @@ -400,3 +541,54 @@ func TestUserConfigDir(t *testing.T) { assert.Equal(t, home, dir) } } + +func TestMasterKey_Identities_Passphrase(t *testing.T) { + t.Run(SopsAgeKeyEnv, func(t *testing.T) { + key := &MasterKey{EncryptedKey: mockEncryptedKey} + t.Setenv(SopsAgeKeyEnv, mockEncryptedIdentity) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + + t.Run(SopsAgeKeyFileEnv, func(t *testing.T) { + tmpDir := t.TempDir() + // Overwrite to ensure local config is not picked up by tests + overwriteUserConfigDir(t, tmpDir) + + keyPath := filepath.Join(tmpDir, "keys.txt") + assert.NoError(t, os.WriteFile(keyPath, []byte(mockEncryptedIdentity), 0o644)) + + key := &MasterKey{EncryptedKey: mockEncryptedKey} + t.Setenv(SopsAgeKeyFileEnv, keyPath) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.NoError(t, err) + assert.EqualValues(t, mockEncryptedKeyPlain, got) + }) + + t.Run("invalid encrypted key", func(t *testing.T) { + key := &MasterKey{EncryptedKey: "invalid"} + t.Setenv(SopsAgeKeyEnv, mockEncryptedIdentity) + //blocks calling gpg-agent + os.Unsetenv("XDG_RUNTIME_DIR") + testOnlyAgePassword = mockIdentityPassphrase + + got, err := key.Decrypt() + testOnlyAgePassword = "" + + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to create reader for decrypting sops data key with age") + assert.Nil(t, got) + }) +} diff --git a/age/ssh_parse.go b/age/ssh_parse.go new file mode 100644 index 000000000..467afc278 --- /dev/null +++ b/age/ssh_parse.go @@ -0,0 +1,84 @@ +// These functions are similar to those in the age project +// https://github.com/FiloSottile/age/blob/v1.0.0/cmd/age/parse.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "fmt" + "io" + "os" + + "filippo.io/age" + "filippo.io/age/agessh" + "golang.org/x/crypto/ssh" +) + +// readPublicKeyFile attempts to read a public key based on the given private +// key path. It assumes the public key is in the same directory, with the same +// name, but with a ".pub" extension. If the public key cannot be read, an +// error is returned. +func readPublicKeyFile(privateKeyPath string) (ssh.PublicKey, error) { + publicKeyPath := privateKeyPath + ".pub" + f, err := os.Open(publicKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to obtain public %q key for %q SSH key: %w", publicKeyPath, privateKeyPath, err) + } + defer f.Close() + contents, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read %q: %w", publicKeyPath, err) + } + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents) + if err != nil { + return nil, fmt.Errorf("failed to parse %q: %w", publicKeyPath, err) + } + return pubKey, nil +} + +// parseSSHIdentityFromPrivateKeyFile returns an age.Identity from the given +// private key file. If the private key file is encrypted, it will configure +// the identity to prompt for a passphrase. +func parseSSHIdentityFromPrivateKeyFile(keyPath string) (age.Identity, error) { + keyFile, err := os.Open(keyPath) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer keyFile.Close() + contents, err := io.ReadAll(keyFile) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + id, err := agessh.ParseIdentity(contents) + if sshErr, ok := err.(*ssh.PassphraseMissingError); ok { + pubKey := sshErr.PublicKey + if pubKey == nil { + pubKey, err = readPublicKeyFile(keyPath) + if err != nil { + return nil, err + } + } + passphrasePrompt := func() ([]byte, error) { + pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", keyPath)) + if err != nil { + return nil, fmt.Errorf("could not read passphrase for %q: %v", keyPath, err) + } + return pass, nil + } + i, err := agessh.NewEncryptedSSHIdentity(pubKey, contents, passphrasePrompt) + if err != nil { + return nil, fmt.Errorf("could not create encrypted SSH identity: %w", err) + } + return i, nil + } + if err != nil { + return nil, fmt.Errorf("malformed SSH identity in %q: %w", keyPath, err) + } + return id, nil +} diff --git a/age/tui.go b/age/tui.go new file mode 100644 index 000000000..35f9f3ad7 --- /dev/null +++ b/age/tui.go @@ -0,0 +1,173 @@ +// These functions have been copied from the age project +// https://github.com/FiloSottile/age/blob/3d91014ea095e8d70f7c6c4833f89b53a96e0832/cmd/age/tui.go +// +// Copyright 2021 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in age's LICENSE file at +// https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE +// +// SPDX-License-Identifier: BSD-3-Clause + +package age + +import ( + "errors" + "filippo.io/age/plugin" + "fmt" + "io" + "os" + "runtime" + "testing" + + "golang.org/x/term" +) + +var testOnlyAgePassword string + +func printf(format string, v ...interface{}) { + log.Printf("age: "+format, v...) +} + +func warningf(format string, v ...interface{}) { + log.Printf("age: warning: "+format, v...) +} + +// clearLine clears the current line on the terminal, or opens a new line if +// terminal escape codes don't work. +func clearLine(out io.Writer) { + const ( + CUI = "\033[" // Control Sequence Introducer + CPL = CUI + "F" // Cursor Previous Line + EL = CUI + "K" // Erase in Line + ) + + // First, open a new line, which is guaranteed to work everywhere. Then, try + // to erase the line above with escape codes. + // + // (We use CRLF instead of LF to work around an apparent bug in WSL2's + // handling of CONOUT$. Only when running a Windows binary from WSL2, the + // cursor would not go back to the start of the line with a simple LF. + // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.) + fmt.Fprintf(out, "\r\n"+CPL+EL) +} + +// withTerminal runs f with the terminal input and output files, if available. +// withTerminal does not open a non-terminal stdin, so the caller does not need +// to check stdinInUse. +func withTerminal(f func(in, out *os.File) error) error { + if runtime.GOOS == "windows" { + in, err := os.OpenFile("CONIN$", os.O_RDWR, 0) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0) + if err != nil { + return err + } + defer out.Close() + return f(in, out) + } else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { + defer tty.Close() + return f(tty, tty) + } else if term.IsTerminal(int(os.Stdin.Fd())) { + return f(os.Stdin, os.Stdin) + } else { + return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err) + } +} + +// readSecret reads a value from the terminal with no echo. The prompt is ephemeral. +func readSecret(prompt string) (s []byte, err error) { + if testing.Testing() { + if testOnlyAgePassword != "" { + return []byte(testOnlyAgePassword), nil + } + } + + err = withTerminal(func(in, out *os.File) error { + fmt.Fprintf(out, "%s ", prompt) + defer clearLine(out) + s, err = term.ReadPassword(int(in.Fd())) + return err + }) + return +} + +// readCharacter reads a single character from the terminal with no echo. The +// prompt is ephemeral. +func readCharacter(prompt string) (c byte, err error) { + err = withTerminal(func(in, out *os.File) error { + fmt.Fprintf(out, "%s ", prompt) + defer clearLine(out) + + oldState, err := term.MakeRaw(int(in.Fd())) + if err != nil { + return err + } + defer term.Restore(int(in.Fd()), oldState) + + b := make([]byte, 1) + if _, err := in.Read(b); err != nil { + return err + } + + c = b[0] + return nil + }) + return +} + +var pluginTerminalUI = &plugin.ClientUI{ + DisplayMessage: func(name, message string) error { + printf("%s plugin: %s", name, message) + return nil + }, + RequestValue: func(name, message string, _ bool) (s string, err error) { + defer func() { + if err != nil { + warningf("could not read value for age-plugin-%s: %v", name, err) + } + }() + secret, err := readSecret(message) + if err != nil { + return "", err + } + return string(secret), nil + }, + Confirm: func(name, message, yes, no string) (choseYes bool, err error) { + defer func() { + if err != nil { + warningf("could not read value for age-plugin-%s: %v", name, err) + } + }() + if no == "" { + message += fmt.Sprintf(" (press enter for %q)", yes) + _, err := readSecret(message) + if err != nil { + return false, err + } + return true, nil + } + message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no) + for { + selection, err := readCharacter(message) + if err != nil { + return false, err + } + switch selection { + case '1': + return true, nil + case '2': + return false, nil + case '\x03': // CTRL-C + return false, errors.New("user cancelled prompt") + default: + warningf("reading value for age-plugin-%s: invalid selection %q", name, selection) + } + } + }, + WaitTimer: func(name string) { + printf("waiting on %s plugin...", name) + }, +} diff --git a/azkv/keysource.go b/azkv/keysource.go index 27e28cca5..28cb6ebde 100644 --- a/azkv/keysource.go +++ b/azkv/keysource.go @@ -76,9 +76,10 @@ func NewMasterKey(vaultURL string, keyName string, keyVersion string) *MasterKey // NewMasterKeyFromURL takes an Azure Key Vault key URL, and returns a new // MasterKey. The URL format is {vaultUrl}/keys/{keyName}/{keyVersion}. func NewMasterKeyFromURL(url string) (*MasterKey, error) { + url = strings.TrimSpace(url) re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)/([^/]+)$") parts := re.FindStringSubmatch(url) - if parts == nil || len(parts) < 3 { + if len(parts) < 3 { return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey", url) } return NewMasterKey(parts[1], parts[2], parts[3]), nil diff --git a/azkv/keysource_test.go b/azkv/keysource_test.go index ef6aa6893..5560e4be6 100644 --- a/azkv/keysource_test.go +++ b/azkv/keysource_test.go @@ -88,6 +88,23 @@ func TestMasterKeysFromURLs(t *testing.T) { }, }, }, + { + name: "multiple URLs with leading and trailing spaces", + urls: " https://test.vault.azure.net/keys/test-key/a2a690a4fcc04166b739da342a912c90 , https://test2.vault.azure.net/keys/another-test-key/cf0021e8b743453bae758e7fbf71b60e ", + expectKeyCount: 2, + expectKeys: []MasterKey{ + { + VaultURL: "https://test.vault.azure.net", + Name: "test-key", + Version: "a2a690a4fcc04166b739da342a912c90", + }, + { + VaultURL: "https://test2.vault.azure.net", + Name: "another-test-key", + Version: "cf0021e8b743453bae758e7fbf71b60e", + }, + }, + }, { name: "multiple URLs, one malformed", urls: "https://test.vault.azure.net/keys/test-key/a2a690a4fcc04166b739da342a912c90,https://test.vault.azure.net/no-keys-here/test-key/a2a690a4fcc04166b739da342a912c90", diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index eecd34423..6d6fa0751 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "io" "os" "path/filepath" "time" @@ -130,11 +131,20 @@ func EncryptTree(opts EncryptTreeOpts) error { return nil } -// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree -func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) { - fileBytes, err := os.ReadFile(inputPath) - if err != nil { - return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) +// LoadEncryptedFileEx loads an encrypted SOPS file from a file or stdin, returning a SOPS tree +func LoadEncryptedFileEx(loader sops.EncryptedFileLoader, inputPath string, readFromStdin bool) (*sops.Tree, error) { + var fileBytes []byte + var err error + if readFromStdin { + fileBytes, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + } + } else { + fileBytes, err = os.ReadFile(inputPath) + if err != nil { + return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + } } path, err := filepath.Abs(inputPath) if err != nil { @@ -145,6 +155,11 @@ func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops return &tree, err } +// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree +func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) { + return LoadEncryptedFileEx(loader, inputPath, false) +} + // NewExitError returns a cli.ExitError given an error (wrapped in a generic interface{}) // and an exit code to represent the failure func NewExitError(i interface{}, exitCode int) *cli.ExitError { @@ -207,7 +222,7 @@ func GetKMSKeyWithEncryptionCtx(tree *sops.Tree) (keyGroupIndex int, keyIndex in for n, k := range kg { kmsKey, ok := k.(*kms.MasterKey) if ok { - if kmsKey.EncryptionContext != nil && len(kmsKey.EncryptionContext) >= 2 { + if len(kmsKey.EncryptionContext) >= 2 { duplicateValues := map[string]int{} for _, v := range kmsKey.EncryptionContext { duplicateValues[*v] = duplicateValues[*v] + 1 @@ -227,6 +242,7 @@ type GenericDecryptOpts struct { Cipher sops.Cipher InputStore sops.Store InputPath string + ReadFromStdin bool IgnoreMAC bool KeyServices []keyservice.KeyServiceClient DecryptionOrder []string @@ -235,7 +251,7 @@ type GenericDecryptOpts struct { // LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes // check for the issue described in https://github.com/mozilla/sops/pull/435 func LoadEncryptedFileWithBugFixes(opts GenericDecryptOpts) (*sops.Tree, error) { - tree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath) + tree, err := LoadEncryptedFileEx(opts.InputStore, opts.InputPath, opts.ReadFromStdin) if err != nil { return nil, err } @@ -447,3 +463,17 @@ func PrettyPrintDiffs(diffs []Diff) { } } } + +// PrettyPrintShamirDiff prints changes in shamir_threshold to stdout +func PrettyPrintShamirDiff(oldValue, newValue int) { + if oldValue > 0 && oldValue == newValue { + fmt.Printf("shamir_threshold: %d\n", newValue) + } else { + if newValue > 0 { + color.New(color.FgGreen).Printf("+++ shamir_threshold: %d\n", newValue) + } + if oldValue > 0 { + color.New(color.FgRed).Printf("--- shamir_threshold: %d\n", oldValue) + } + } +} diff --git a/cmd/sops/decrypt.go b/cmd/sops/decrypt.go index db038787b..d0da0ddf1 100644 --- a/cmd/sops/decrypt.go +++ b/cmd/sops/decrypt.go @@ -19,6 +19,7 @@ type decryptOpts struct { InputStore sops.Store OutputStore sops.Store InputPath string + ReadFromStdin bool IgnoreMAC bool Extract []interface{} KeyServices []keyservice.KeyServiceClient @@ -27,11 +28,12 @@ type decryptOpts struct { func decryptTree(opts decryptOpts) (tree *sops.Tree, err error) { tree, err = common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ - Cipher: opts.Cipher, - InputStore: opts.InputStore, - InputPath: opts.InputPath, - IgnoreMAC: opts.IgnoreMAC, - KeyServices: opts.KeyServices, + Cipher: opts.Cipher, + InputStore: opts.InputStore, + InputPath: opts.InputPath, + ReadFromStdin: opts.ReadFromStdin, + IgnoreMAC: opts.IgnoreMAC, + KeyServices: opts.KeyServices, }) if err != nil { return nil, err diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 982cfb967..311c8921a 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -245,7 +245,12 @@ func hashFile(filePath string) ([]byte, error) { } func runEditor(path string) error { - editor := os.Getenv("EDITOR") + envVar := "SOPS_EDITOR" + editor := os.Getenv(envVar) + if editor == "" { + envVar = "EDITOR" + editor = os.Getenv(envVar) + } var cmd *exec.Cmd if editor == "" { editor, err := lookupAnyEditor("vim", "nano", "vi") @@ -256,7 +261,7 @@ func runEditor(path string) error { } else { parts, err := shlex.Split(editor) if err != nil { - return fmt.Errorf("invalid $EDITOR: %s", editor) + return fmt.Errorf("invalid $%s: %s", envVar, editor) } parts = append(parts, path) cmd = exec.Command(parts[0], parts[1:]...) @@ -275,5 +280,5 @@ func lookupAnyEditor(editorNames ...string) (editorPath string, err error) { return editorPath, nil } } - return "", fmt.Errorf("no editor available: sops attempts to use the editor defined in the EDITOR environment variable, and if that's not set defaults to any of %s, but none of them could be found", strings.Join(editorNames, ", ")) + return "", fmt.Errorf("no editor available: sops attempts to use the editor defined in the SOPS_EDITOR or EDITOR environment variables, and if that's not set defaults to any of %s, but none of them could be found", strings.Join(editorNames, ", ")) } diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index ace7d8c2c..e5ffc6950 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" "path/filepath" @@ -27,11 +28,12 @@ type encryptConfig struct { } type encryptOpts struct { - Cipher sops.Cipher - InputStore sops.Store - OutputStore sops.Store - InputPath string - KeyServices []keyservice.KeyServiceClient + Cipher sops.Cipher + InputStore sops.Store + OutputStore sops.Store + InputPath string + ReadFromStdin bool + KeyServices []keyservice.KeyServiceClient encryptConfig } @@ -78,9 +80,17 @@ func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata { func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { // Load the file - fileBytes, err := os.ReadFile(opts.InputPath) - if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + var fileBytes []byte + if opts.ReadFromStdin { + fileBytes, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, common.NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile) + } + } else { + fileBytes, err = os.ReadFile(opts.InputPath) + if err != nil { + return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile) + } } branches, err := opts.InputStore.LoadPlainFile(fileBytes) if err != nil { diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 155eaec78..b9a26b15c 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -137,7 +137,8 @@ func main() { To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC. - To select a different editor than the default (vim), set EDITOR. + To select a different editor than the default (vim), set SOPS_EDITOR or + EDITOR. Note that flags must always be provided before the filename to operate on. Otherwise, they will be ignored. @@ -162,6 +163,10 @@ func main() { Name: "user", Usage: "the user to run the command as", }, + cli.BoolFlag{ + Name: "same-process", + Usage: "run command in the current process instead of in a child process", + }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.NArg() != 2 { @@ -171,7 +176,10 @@ func main() { fileName := c.Args()[0] command := c.Args()[1] - inputStore := inputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) @@ -191,6 +199,10 @@ func main() { if c.Bool("background") { log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops") + + if c.Bool("same-process") { + return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters) + } } tree, err := decryptTree(opts) @@ -221,12 +233,13 @@ func main() { } if err := exec.ExecWithEnv(exec.ExecOpts{ - Command: command, - Plaintext: []byte{}, - Background: c.Bool("background"), - Pristine: c.Bool("pristine"), - User: c.String("user"), - Env: env, + Command: command, + Plaintext: []byte{}, + Background: c.Bool("background"), + Pristine: c.Bool("pristine"), + User: c.String("user"), + SameProcess: c.Bool("same-process"), + Env: env, }); err != nil { return toExitError(err) } @@ -272,8 +285,14 @@ func main() { fileName := c.Args()[0] command := c.Args()[1] - inputStore := inputStore(c, fileName) - outputStore := outputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) @@ -340,9 +359,15 @@ func main() { if c.Bool("verbose") || c.GlobalBool("verbose") { logging.SetLevel(logrus.DebugLevel) } - configPath, err := config.FindConfigFile(".") - if err != nil { - return common.NewExitError(err, codes.ErrorGeneric) + var configPath string + var err error + if c.GlobalString("config") != "" { + configPath = c.GlobalString("config") + } else { + configPath, err = config.FindConfigFile(".") + if err != nil { + return common.NewExitError(err, codes.ErrorGeneric) + } } if c.NArg() < 1 { return common.NewExitError("Error: no file specified", codes.NoFileSpecified) @@ -365,13 +390,17 @@ func main() { return toExitError(err) } if !info.IsDir() { + inputStore, err := inputStore(c, subPath) + if err != nil { + return toExitError(err) + } err = publishcmd.Run(publishcmd.Opts{ ConfigPath: configPath, InputPath: subPath, Cipher: aes.NewCipher(), KeyServices: keyservices(c), DecryptionOrder: order, - InputStore: inputStore(c, subPath), + InputStore: inputStore, Interactive: !c.Bool("yes"), OmitExtensions: c.Bool("omit-extensions"), Recursive: c.Bool("recursive"), @@ -433,14 +462,22 @@ func main() { Name: "filestatus", Usage: "check the status of the file, returning encryption status", ArgsUsage: `file`, - Flags: []cli.Flag{}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "input-type", + Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type", + }, + }, Action: func(c *cli.Context) error { if c.NArg() < 1 { return common.NewExitError("Error: no file specified", codes.NoFileSpecified) } fileName := c.Args()[0] - inputStore := inputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } opts := filestatuscmd.Opts{ InputStore: inputStore, InputPath: fileName, @@ -560,11 +597,19 @@ func main() { group = append(group, key) } } + inputStore, err := inputStore(c, c.String("file")) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, c.String("file")) + if err != nil { + return toExitError(err) + } return groups.Add(groups.AddOpts{ InputPath: c.String("file"), InPlace: c.Bool("in-place"), - InputStore: inputStore(c, c.String("file")), - OutputStore: outputStore(c, c.String("file")), + InputStore: inputStore, + OutputStore: outputStore, Group: group, GroupThreshold: c.Int("shamir-secret-sharing-threshold"), KeyServices: keyservices(c), @@ -599,11 +644,19 @@ func main() { return fmt.Errorf("failed to parse [index] argument: %s", err) } + inputStore, err := inputStore(c, c.String("file")) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, c.String("file")) + if err != nil { + return toExitError(err) + } return groups.Delete(groups.DeleteOpts{ InputPath: c.String("file"), InPlace: c.Bool("in-place"), - InputStore: inputStore(c, c.String("file")), - OutputStore: outputStore(c, c.String("file")), + InputStore: inputStore, + OutputStore: outputStore, Group: uint(group), GroupThreshold: c.Int("shamir-secret-sharing-threshold"), KeyServices: keyservices(c), @@ -643,12 +696,12 @@ func main() { failedCounter := 0 for _, path := range c.Args() { err := updatekeys.UpdateKeys(updatekeys.Opts{ - InputPath: path, - GroupQuorum: c.Int("shamir-secret-sharing-quorum"), - KeyServices: keyservices(c), - Interactive: !c.Bool("yes"), - ConfigPath: configPath, - InputType: c.String("input-type"), + InputPath: path, + ShamirThreshold: c.Int("shamir-secret-sharing-threshold"), + KeyServices: keyservices(c), + Interactive: !c.Bool("yes"), + ConfigPath: configPath, + InputType: c.String("input-type"), }) if c.NArg() == 1 { @@ -675,8 +728,8 @@ func main() { }, { Name: "decrypt", - Usage: "decrypt a file, and output the results to stdout", - ArgsUsage: `file`, + Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, Flags: append([]cli.Flag{ cli.BoolFlag{ Name: "in-place, i", @@ -704,7 +757,7 @@ func main() { }, cli.StringFlag{ Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Should be provided when reading from stdin.", }, cli.StringFlag{ Name: "decryption-order", @@ -716,27 +769,43 @@ func main() { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + readFromStdin := c.NArg() == 0 + if readFromStdin && c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified) + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args()[0]) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } - inputStore := inputStore(c, fileNameOverride) - outputStore := outputStore(c, fileNameOverride) + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) order, err := decryptionOrder(c.String("decryption-order")) @@ -753,6 +822,7 @@ func main() { OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, + ReadFromStdin: readFromStdin, Cipher: aes.NewCipher(), Extract: extract, KeyServices: svcs, @@ -794,8 +864,8 @@ func main() { }, { Name: "encrypt", - Usage: "encrypt a file, and output the results to stdout", - ArgsUsage: `file`, + Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.", + ArgsUsage: `[file]`, Flags: append([]cli.Flag{ cli.BoolFlag{ Name: "in-place, i", @@ -873,34 +943,55 @@ func main() { }, cli.StringFlag{ Name: "filename-override", - Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", + Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.", }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() < 1 { - return common.NewExitError("Error: no file specified", codes.NoFileSpecified) + readFromStdin := c.NArg() == 0 + if readFromStdin { + if c.Bool("in-place") { + return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters) + } + if c.String("filename-override") == "" { + return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters) + } } warnMoreThanOnePositionalArgument(c) if c.Bool("in-place") && c.String("output") != "" { return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters) } - fileName, err := filepath.Abs(c.Args()[0]) - if err != nil { - return toExitError(err) - } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified) + var fileName string + var err error + if !readFromStdin { + fileName, err = filepath.Abs(c.Args()[0]) + if err != nil { + return toExitError(err) + } + if _, err := os.Stat(fileName); os.IsNotExist(err) { + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) + } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } - inputStore := inputStore(c, fileNameOverride) - outputStore := outputStore(c, fileNameOverride) + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) encConfig, err := getEncryptConfig(c, fileNameOverride) @@ -911,6 +1002,7 @@ func main() { OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, + ReadFromStdin: readFromStdin, Cipher: aes.NewCipher(), KeyServices: svcs, encryptConfig: encConfig, @@ -1050,16 +1142,27 @@ func main() { if _, err := os.Stat(fileName); os.IsNotExist(err) { if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError("Error: cannot add or remove keys on non-existent files, use the `edit` subcommand instead.", codes.CannotChangeKeysFromNonExistentFile) + return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } - inputStore := inputStore(c, fileNameOverride) - outputStore := outputStore(c, fileNameOverride) + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) order, err := decryptionOrder(c.String("decryption-order")) @@ -1202,12 +1305,15 @@ func main() { if err != nil { return toExitError(err) } - if _, err := os.Stat(fileName); os.IsNotExist(err) { - return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified) - } - inputStore := inputStore(c, fileName) - outputStore := outputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) order, err := decryptionOrder(c.String("decryption-order")) @@ -1288,6 +1394,10 @@ func main() { Usage: "comma separated list of decryption key types", EnvVar: "SOPS_DECRYPTION_ORDER", }, + cli.BoolFlag{ + Name: "idempotent", + Usage: "do nothing if the given index already has the given value", + }, }, keyserviceFlags...), Action: func(c *cli.Context) error { if c.Bool("verbose") { @@ -1301,8 +1411,14 @@ func main() { return toExitError(err) } - inputStore := inputStore(c, fileName) - outputStore := outputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) path, err := parseTreePath(c.Args()[1]) @@ -1319,7 +1435,7 @@ func main() { if err != nil { return toExitError(err) } - output, err := set(setOpts{ + output, changed, err := set(setOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, @@ -1334,6 +1450,11 @@ func main() { return toExitError(err) } + if !changed && c.Bool("idempotent") { + log.Info("File not written due to no change") + return nil + } + // We open the file *after* the operations on the tree have been // executed to avoid truncating it when there's errors file, err := os.Create(fileName) @@ -1376,8 +1497,8 @@ func main() { EnvVar: "SOPS_DECRYPTION_ORDER", }, cli.BoolFlag{ - Name: "idempotent", - Usage: "do nothing if the given index does not exist", + Name: "idempotent", + Usage: "do nothing if the given index does not exist", }, }, keyserviceFlags...), Action: func(c *cli.Context) error { @@ -1392,8 +1513,14 @@ func main() { return toExitError(err) } - inputStore := inputStore(c, fileName) - outputStore := outputStore(c, fileName) + inputStore, err := inputStore(c, fileName) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileName) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) path, err := parseTreePath(c.Args()[1]) @@ -1452,8 +1579,9 @@ func main() { Usage: "generate a new data encryption key and reencrypt all values with the new key", }, cli.BoolFlag{ - Name: "disable-version-check", - Usage: "do not check whether the current version is latest during --version", + Name: "disable-version-check", + Usage: "do not check whether the current version is latest during --version", + EnvVar: "SOPS_DISABLE_VERSION_CHECK", }, cli.StringFlag{ Name: "kms, k", @@ -1592,6 +1720,7 @@ func main() { cli.StringFlag{ Name: "config", Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", + EnvVar: "SOPS_CONFIG", }, cli.StringFlag{ Name: "encryption-context", @@ -1652,15 +1781,20 @@ func main() { if _, err := os.Stat(fileName); os.IsNotExist(err) { if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" || c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" { - return common.NewExitError("Error: cannot add or remove keys on non-existent files, use `--kms` and `--pgp` instead.", codes.CannotChangeKeysFromNonExistentFile) + return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile) } if isEncryptMode || isDecryptMode || isRotateMode { - return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified) + return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified) } } fileNameOverride := c.String("filename-override") if fileNameOverride == "" { fileNameOverride = fileName + } else { + fileNameOverride, err = filepath.Abs(fileNameOverride) + if err != nil { + return toExitError(err) + } } commandCount := 0 @@ -1690,8 +1824,14 @@ func main() { } } - inputStore := inputStore(c, fileNameOverride) - outputStore := outputStore(c, fileNameOverride) + inputStore, err := inputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } + outputStore, err := outputStore(c, fileNameOverride) + if err != nil { + return toExitError(err) + } svcs := keyservices(c) order, err := decryptionOrder(c.String("decryption-order")) @@ -1758,7 +1898,7 @@ func main() { if err != nil { return toExitError(err) } - output, err = set(setOpts{ + output, _, err = set(setOpts{ OutputStore: outputStore, InputStore: inputStore, InputPath: fileName, @@ -2030,7 +2170,7 @@ func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) { "address", fmt.Sprintf("%s://%s", url.Scheme, addr), ).Infof("Connecting to key service") - conn, err := grpc.Dial(addr, opts...) + conn, err := grpc.NewClient(addr, opts...) if err != nil { log.Fatalf("failed to listen: %v", err) } @@ -2040,10 +2180,8 @@ func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) { } func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, error) { - var configPath string - if context.String("config") != "" { - configPath = context.String("config") - } else { + configPath := context.GlobalString("config") + if configPath == "" { // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory foundPath, err := config.FindConfigFile(".") if err != nil { @@ -2054,13 +2192,19 @@ func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, return config.LoadStoresConfig(configPath) } -func inputStore(context *cli.Context, path string) common.Store { - storesConf, _ := loadStoresConfig(context, path) - return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("input-type")) +func inputStore(context *cli.Context, path string) (common.Store, error) { + storesConf, err := loadStoresConfig(context, path) + if err != nil { + return nil, err + } + return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("input-type")), nil } -func outputStore(context *cli.Context, path string) common.Store { - storesConf, _ := loadStoresConfig(context, path) +func outputStore(context *cli.Context, path string) (common.Store, error) { + storesConf, err := loadStoresConfig(context, path) + if err != nil { + return nil, err + } if context.IsSet("indent") { indent := context.Int("indent") storesConf.YAML.Indent = indent @@ -2068,7 +2212,7 @@ func outputStore(context *cli.Context, path string) common.Store { storesConf.JSONBinary.Indent = indent } - return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("output-type")) + return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("output-type")), nil } func parseTreePath(arg string) ([]interface{}, error) { @@ -2159,7 +2303,7 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { if err != nil { errMsg = fmt.Sprintf("%s: %s", errMsg, err) } - return nil, fmt.Errorf(errMsg) + return nil, fmt.Errorf("%s", errMsg) } return conf.KeyGroups, err } @@ -2178,10 +2322,8 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) { // Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) { var err error - var configPath string - if c.String("config") != "" { - configPath = c.String("config") - } else { + configPath := c.GlobalString("config") + if configPath == "" { // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory configPath, err = config.FindConfigFile(".") if err != nil { diff --git a/cmd/sops/set.go b/cmd/sops/set.go index a6e8ed357..7a7da298d 100644 --- a/cmd/sops/set.go +++ b/cmd/sops/set.go @@ -21,7 +21,7 @@ type setOpts struct { DecryptionOrder []string } -func set(opts setOpts) ([]byte, error) { +func set(opts setOpts) ([]byte, bool, error) { // Load the file // TODO: Issue #173: if the file does not exist, create it with the contents passed in as opts.Value tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ @@ -32,7 +32,7 @@ func set(opts setOpts) ([]byte, error) { KeyServices: opts.KeyServices, }) if err != nil { - return nil, err + return nil, false, err } // Decrypt the file @@ -44,22 +44,23 @@ func set(opts setOpts) ([]byte, error) { DecryptionOrder: opts.DecryptionOrder, }) if err != nil { - return nil, err + return nil, false, err } // Set the value - tree.Branches[0] = tree.Branches[0].Set(opts.TreePath, opts.Value) + var changed bool + tree.Branches[0], changed = tree.Branches[0].Set(opts.TreePath, opts.Value) err = common.EncryptTree(common.EncryptTreeOpts{ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher, }) if err != nil { - return nil, err + return nil, false, err } encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree) if err != nil { - return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) + return nil, false, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree) } - return encryptedFile, err + return encryptedFile, changed, err } diff --git a/cmd/sops/subcommand/exec/exec.go b/cmd/sops/subcommand/exec/exec.go index be74a31a4..3ac7cfd63 100644 --- a/cmd/sops/subcommand/exec/exec.go +++ b/cmd/sops/subcommand/exec/exec.go @@ -2,6 +2,7 @@ package exec import ( "bytes" + "fmt" "os" "path/filepath" "runtime" @@ -23,14 +24,15 @@ func init() { } type ExecOpts struct { - Command string - Plaintext []byte - Background bool - Pristine bool - Fifo bool - User string - Filename string - Env []string + Command string + Plaintext []byte + Background bool + SameProcess bool + Pristine bool + Fifo bool + User string + Filename string + Env []string } func GetFile(dir, filename string) *os.File { @@ -115,6 +117,10 @@ func ExecWithEnv(opts ExecOpts) error { SwitchUser(opts.User) } + if runtime.GOOS == "windows" && opts.SameProcess { + return fmt.Errorf("The --same-process flag is not supported on Windows") + } + var env []string if !opts.Pristine { @@ -134,6 +140,15 @@ func ExecWithEnv(opts ExecOpts) error { env = append(env, opts.Env...) + if opts.SameProcess { + if opts.Background { + log.Fatal("background is not supported for same-process") + } + + // Note that the call does NOT return, unless an error happens. + return ExecSyscall(opts.Command, env) + } + cmd := BuildCommand(opts.Command) cmd.Env = env diff --git a/cmd/sops/subcommand/exec/exec_unix.go b/cmd/sops/subcommand/exec/exec_unix.go index cc831e798..a7f63ce8d 100644 --- a/cmd/sops/subcommand/exec/exec_unix.go +++ b/cmd/sops/subcommand/exec/exec_unix.go @@ -11,6 +11,10 @@ import ( "syscall" ) +func ExecSyscall(command string, env []string) error { + return syscall.Exec("/bin/sh", []string{"/bin/sh", "-c", command}, env) +} + func BuildCommand(command string) *exec.Cmd { return exec.Command("/bin/sh", "-c", command) } diff --git a/cmd/sops/subcommand/exec/exec_windows.go b/cmd/sops/subcommand/exec/exec_windows.go index 7e0f21d74..a510f2826 100644 --- a/cmd/sops/subcommand/exec/exec_windows.go +++ b/cmd/sops/subcommand/exec/exec_windows.go @@ -4,6 +4,11 @@ import ( "os/exec" ) +func ExecSyscall(command string, env []string) error { + log.Fatal("same-process not available on windows") + return nil +} + func BuildCommand(command string) *exec.Cmd { return exec.Command("cmd.exe", "/C", command) } diff --git a/cmd/sops/subcommand/updatekeys/updatekeys.go b/cmd/sops/subcommand/updatekeys/updatekeys.go index 4b01e8ab7..9dec066a6 100644 --- a/cmd/sops/subcommand/updatekeys/updatekeys.go +++ b/cmd/sops/subcommand/updatekeys/updatekeys.go @@ -15,7 +15,7 @@ import ( // Opts represents key operation options and config type Opts struct { InputPath string - GroupQuorum int + ShamirThreshold int KeyServices []keyservice.KeyServiceClient DecryptionOrder []string Interactive bool @@ -45,7 +45,7 @@ func updateFile(opts Opts) error { if err != nil { return err } - store := common.DefaultStoreForPath(sc, opts.InputPath) + store := common.DefaultStoreForPathOrFormat(sc, opts.InputPath, opts.InputType) log.Printf("Syncing keys for file %s", opts.InputPath) tree, err := common.LoadEncryptedFile(store, opts.InputPath) if err != nil { @@ -66,11 +66,22 @@ func updateFile(opts Opts) error { keysWillChange = true } } - if !keysWillChange { + + // TODO: use conf.ShamirThreshold instead of tree.Metadata.ShamirThreshold in the next line? + // Or make this configurable? + var shamirThreshold = tree.Metadata.ShamirThreshold + if opts.ShamirThreshold != 0 { + shamirThreshold = opts.ShamirThreshold + } + shamirThreshold = min(shamirThreshold, len(conf.KeyGroups)) + var shamirThresholdWillChange = tree.Metadata.ShamirThreshold != shamirThreshold + + if !keysWillChange && !shamirThresholdWillChange { log.Printf("File %s already up to date", opts.InputPath) return nil } fmt.Printf("The following changes will be made to the file's groups:\n") + common.PrettyPrintShamirDiff(tree.Metadata.ShamirThreshold, shamirThreshold) common.PrettyPrintDiffs(diffs) if opts.Interactive { @@ -92,10 +103,7 @@ func updateFile(opts Opts) error { return common.NewExitError(err, codes.CouldNotRetrieveKey) } tree.Metadata.KeyGroups = conf.KeyGroups - if opts.GroupQuorum != 0 { - tree.Metadata.ShamirThreshold = opts.GroupQuorum - } - tree.Metadata.ShamirThreshold = min(tree.Metadata.ShamirThreshold, len(tree.Metadata.KeyGroups)) + tree.Metadata.ShamirThreshold = shamirThreshold errs := tree.Metadata.UpdateMasterKeysWithKeyServices(key, opts.KeyServices) if len(errs) > 0 { return fmt.Errorf("error updating one or more master keys: %s", errs) diff --git a/config/config.go b/config/config.go index 9620c0da9..8d3dc4dd1 100644 --- a/config/config.go +++ b/config/config.go @@ -17,19 +17,11 @@ import ( "github.com/getsops/sops/v3/gcpkms" "github.com/getsops/sops/v3/hcvault" "github.com/getsops/sops/v3/kms" - "github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/publish" - "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) -var log *logrus.Logger - -func init() { - log = logging.NewLogger("CONFIG") -} - type fileSystem interface { Stat(name string) (os.FileInfo, error) } @@ -377,19 +369,17 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio } var dest publish.Destination - if dRule != nil { - if dRule.S3Bucket != "" && dRule.GCSBucket != "" && dRule.VaultPath != "" { - return nil, fmt.Errorf("error loading config: more than one destinations were found in a single destination rule, you can only use one per rule") - } - if dRule.S3Bucket != "" { - dest = publish.NewS3Destination(dRule.S3Bucket, dRule.S3Prefix) - } - if dRule.GCSBucket != "" { - dest = publish.NewGCSDestination(dRule.GCSBucket, dRule.GCSPrefix) - } - if dRule.VaultPath != "" { - dest = publish.NewVaultDestination(dRule.VaultAddress, dRule.VaultPath, dRule.VaultKVMountName, dRule.VaultKVVersion) - } + if dRule.S3Bucket != "" && dRule.GCSBucket != "" && dRule.VaultPath != "" { + return nil, fmt.Errorf("error loading config: more than one destinations were found in a single destination rule, you can only use one per rule") + } + if dRule.S3Bucket != "" { + dest = publish.NewS3Destination(dRule.S3Bucket, dRule.S3Prefix) + } + if dRule.GCSBucket != "" { + dest = publish.NewGCSDestination(dRule.GCSBucket, dRule.GCSPrefix) + } + if dRule.VaultPath != "" { + dest = publish.NewVaultDestination(dRule.VaultAddress, dRule.VaultPath, dRule.VaultKVMountName, dRule.VaultKVVersion) } config, err := configFromRule(rule, kmsEncryptionContext) diff --git a/config/config_test.go b/config/config_test.go index abf2c66a5..9ac63645a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,10 +1,12 @@ package config import ( + "fmt" "os" "path" "testing" + "github.com/getsops/sops/v3/keys" "github.com/stretchr/testify/assert" ) @@ -94,6 +96,20 @@ creation_rules: - kms: - arn: foo aws_profile: bar + - arn: foo + context: + baz: bam + - arn: foo + aws_profile: bar + context: + baz: bam + - arn: foo + role: '123' + - arn: foo + aws_profile: bar + context: + baz: bam + role: '123' pgp: - bar gcp_kms: @@ -129,113 +145,124 @@ creation_rules: - 'https://foo.vault:8200/v1/foo/keys/foo-key' - merge: - merge: - - kms: + - pgp: # key01 + - foo + kms: + # key02 - arn: foo aws_profile: foo - pgp: - # key02 - - foo - gcp_kms: # key03 + - arn: foo + aws_profile: bar + context: + baz: bam + role: '123' + gcp_kms: + # key04 - resource_id: foo azure_keyvault: - # key04 + # key05 - vaultUrl: https://foo.vault.azure.net key: foo-key version: fooversion hc_vault: - # key05 - - 'https://bar.vault:8200/v1/bar/keys/bar-key' - - kms: # key06 - - arn: bar - aws_profile: bar - pgp: + - 'https://bar.vault:8200/v1/bar/keys/bar-key' + - pgp: # key07 - bar - gcp_kms: + kms: # key08 - - resource_id: bar + - arn: bar + aws_profile: bar + gcp_kms: # key09 + - resource_id: bar + # key10 - resource_id: baz azure_keyvault: - # key10 + # key11 - vaultUrl: https://bar.vault.azure.net key: bar-key version: barversion hc_vault: - # key01 - duplicate#1 + # key12 - 'https://baz.vault:8200/v1/baz/keys/baz-key' + pgp: + # key13 + - baz kms: - # key11 + # key14 - arn: baz aws_profile: baz - pgp: - # key12 - - baz gcp_kms: - # key03 - duplicate#2 - # --> should be removed when loading config + # duplicate of key09 - resource_id: bar azure_keyvault: - # key04 - duplicate#3 + # duplicate of key05 - vaultUrl: https://foo.vault.azure.net key: foo-key version: fooversion hc_vault: - # key13 - duplicate#4 - but from different key_group - # --> should stay + # key15 (duplicate of key00, but that's in a different key_group) - 'https://foo.vault:8200/v1/foo/keys/foo-key' - - kms: - # key14 + - pgp: + # key16 + - qux + kms: + # key17 - arn: qux aws_profile: qux - # key14 - duplicate#5 + # key18 - arn: baz aws_profile: bar - pgp: - # key15 - - qux + # key19 + - arn: baz + role: '123' gcp_kms: - # key16 + # key20 - resource_id: qux - # key17 + # key21 - resource_id: fnord azure_keyvault: - # key18 + # key22 - vaultUrl: https://baz.vault.azure.net key: baz-key version: bazversion hc_vault: - # key19 + # key23 - 'https://qux.vault:8200/v1/qux/keys/qux-key' - # everything below this should be loaded, - # since it is not in a merge block + pgp: + # duplicate of key07 + - bar kms: - # duplicated key06 + # duplicate of key08 - arn: bar aws_profile: bar - # key20 + # key24 - arn: fnord aws_profile: fnord - pgp: - # duplicated key07 - - bar + # duplicate of key03 + - arn: foo + aws_profile: bar + context: + baz: bam + role: '123' gcp_kms: - # duplicated key08 + # duplicate of key09 - resource_id: bar - # key21 + # duplicate of key21 - resource_id: fnord azure_keyvault: - # duplicated key10 + # duplicate of key11 - vaultUrl: https://bar.vault.azure.net key: bar-key version: barversion hc_vault: - # duplicated 'key01 - duplicate#2' + # duplicate of key12 - 'https://baz.vault:8200/v1/baz/keys/baz-key' - # key22 + # key25 - 'https://fnord.vault:8200/v1/fnord/keys/fnord-key' `) @@ -421,6 +448,7 @@ func TestLoadConfigFile(t *testing.T) { } func TestLoadConfigFileWithGroups(t *testing.T) { + bam := "bam" expected := configFile{ CreationRules: []creationRule{ { @@ -432,7 +460,37 @@ func TestLoadConfigFileWithGroups(t *testing.T) { PathRegex: "", KeyGroups: []keyGroup{ { - KMS: []kmsKey{{Arn: "foo", AwsProfile: "bar"}}, + KMS: []kmsKey{ + { + Arn: "foo", + AwsProfile: "bar", + }, + { + Arn: "foo", + Context: map[string]*string{ + "baz": &bam, + }, + }, + { + Arn: "foo", + AwsProfile: "bar", + Context: map[string]*string{ + "baz": &bam, + }, + }, + { + Arn: "foo", + Role: "123", + }, + { + Arn: "foo", + AwsProfile: "bar", + Context: map[string]*string{ + "baz": &bam, + }, + Role: "123", + }, + }, PGP: []string{"bar"}, GCPKMS: []gcpKmsKey{{ResourceID: "foo"}}, AzureKV: []azureKVKey{{VaultURL: "https://foo.vault.azure.net", Key: "foo-key", Version: "fooversion"}}, @@ -459,12 +517,52 @@ func TestLoadConfigFileWithGroups(t *testing.T) { assert.Equal(t, expected, conf) } +func id(key keys.MasterKey) string { + return fmt.Sprintf("%s: %s", key.TypeToIdentifier(), key.ToString()) +} + +func ids(keys []keys.MasterKey) []string { + result := make([]string, 0, len(keys)) + for _, key := range keys { + result = append(result, id(key)) + } + return result +} + func TestLoadConfigFileWithMerge(t *testing.T) { conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithMergeType, t), "/conf/path", "whatever", nil) assert.Nil(t, err) assert.Equal(t, 2, len(conf.KeyGroups)) - assert.Equal(t, 1, len(conf.KeyGroups[0])) - assert.Equal(t, 22, len(conf.KeyGroups[1])) + assert.Equal(t, []string{ + "hc_vault: https://foo.vault:8200/v1/foo/keys/foo-key", + }, ids(conf.KeyGroups[0])) + assert.Equal(t, []string{ + "pgp: foo", // key01 + "kms: foo||foo", //key02 + "kms: foo+123|baz:bam|bar", //key03 + "gcp_kms: foo", //key04 + "azure_kv: https://foo.vault.azure.net/keys/foo-key/fooversion", //key05 + "hc_vault: https://bar.vault:8200/v1/bar/keys/bar-key", //key06 + "pgp: bar", //key07 + "kms: bar||bar", //key08 + "gcp_kms: bar", //key09 + "gcp_kms: baz", //key10 + "azure_kv: https://bar.vault.azure.net/keys/bar-key/barversion", //key11 + "hc_vault: https://baz.vault:8200/v1/baz/keys/baz-key", //key12 + "pgp: baz", //key13 + "kms: baz||baz", //key14 + "hc_vault: https://foo.vault:8200/v1/foo/keys/foo-key", //key15 + "pgp: qux", //key16 + "kms: qux||qux", //key17 + "kms: baz||bar", //key18 + "kms: baz+123", //key19 + "gcp_kms: qux", //key20 + "gcp_kms: fnord", //key21 + "azure_kv: https://baz.vault.azure.net/keys/baz-key/bazversion", //key22 + "hc_vault: https://qux.vault:8200/v1/qux/keys/qux-key", //key23 + "kms: fnord||fnord", //key24 + "hc_vault: https://fnord.vault:8200/v1/fnord/keys/fnord-key", //key25 + }, ids(conf.KeyGroups[1])) } func TestLoadConfigFileWithNoMatchingRules(t *testing.T) { @@ -538,9 +636,13 @@ func TestKeyGroupsForFileWithGroups(t *testing.T) { conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithGroups, t), "/conf/path", "whatever", nil) assert.Nil(t, err) assert.Equal(t, "bar", conf.KeyGroups[0][0].ToString()) - assert.Equal(t, "foo", conf.KeyGroups[0][1].ToString()) + assert.Equal(t, "foo||bar", conf.KeyGroups[0][1].ToString()) + assert.Equal(t, "foo|baz:bam", conf.KeyGroups[0][2].ToString()) + assert.Equal(t, "foo|baz:bam|bar", conf.KeyGroups[0][3].ToString()) + assert.Equal(t, "foo+123", conf.KeyGroups[0][4].ToString()) + assert.Equal(t, "foo+123|baz:bam|bar", conf.KeyGroups[0][5].ToString()) assert.Equal(t, "qux", conf.KeyGroups[1][0].ToString()) - assert.Equal(t, "baz", conf.KeyGroups[1][1].ToString()) + assert.Equal(t, "baz||foo", conf.KeyGroups[1][1].ToString()) } func TestLoadConfigFileWithUnencryptedSuffix(t *testing.T) { diff --git a/docs/release.md b/docs/release.md index 7485b136a..6946751f1 100644 --- a/docs/release.md +++ b/docs/release.md @@ -37,7 +37,7 @@ This configuration is quite sophisticated, and ensures at least the following: `main` branch. At present, this means that all pull requests attached to the milestone for the release are merged. If there are any pull requests that should not be included in the release, move them to a different milestone. -- [ ] Create a pull request to update the [`CHANGELOG.rst`](../CHANGELOG.rst) +- [ ] Create a pull request to update the [`CHANGELOG.md`](../CHANGELOG.md) file. This should include a summary of all changes since the last release, including references to any relevant pull requests. - [ ] In this same pull request, update the version number in `version/version.go` diff --git a/functional-tests/.sops.yaml b/functional-tests/.sops.yaml index 3e346dd5e..17dd2be78 100644 --- a/functional-tests/.sops.yaml +++ b/functional-tests/.sops.yaml @@ -11,6 +11,12 @@ creation_rules: - FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 - pgp: - B611A2F9F11D0FF82568805119F9B5DAEA91FF86 + - path_regex: test_no_keygroups.yaml + - path_regex: test_zero_keygroups.yaml + key_groups: [] + - path_regex: test_empty_keygroup.yaml + key_groups: + - {} - pgp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 destination_rules: - s3_bucket: "sops-publish-functional-tests" diff --git a/functional-tests/Cargo.lock b/functional-tests/Cargo.lock new file mode 100644 index 000000000..a5225cd21 --- /dev/null +++ b/functional-tests/Cargo.lock @@ -0,0 +1,322 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "functional-tests" +version = "0.1.0" +dependencies = [ + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "tempfile", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" + +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] diff --git a/functional-tests/Cargo.toml b/functional-tests/Cargo.toml index 1635bac93..4f2d667a6 100644 --- a/functional-tests/Cargo.toml +++ b/functional-tests/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" authors = ["Adrian Utrilla "] [dependencies] -tempdir = "0.3.5" +tempfile = "3.19.1" serde = "1.0" -serde_json = "1.0.99" -serde_yaml = "0.9.22" +serde_json = "1.0.140" +serde_yaml = "0.9.34" serde_derive = "1.0" -lazy_static = "1.4.0" +lazy_static = "1.5.0" diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index cca673edb..ad3034116 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -1,11 +1,9 @@ +#[cfg_attr(test, macro_use)] +extern crate lazy_static; extern crate serde; extern crate serde_json; extern crate serde_yaml; -extern crate tempdir; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate serde_derive; +extern crate tempfile; #[cfg(test)] mod tests { @@ -16,10 +14,11 @@ mod tests { use serde_yaml::Value; use std::env; use std::fs::File; - use std::io::{Read, Write}; + use std::io::{BufWriter, Read, Write}; use std::path::Path; - use std::process::Command; - use tempdir::TempDir; + use std::process::{Child, Command, Stdio}; + use tempfile::Builder; + use tempfile::TempDir; const SOPS_BINARY_PATH: &'static str = "./sops"; const KMS_KEY: &'static str = "FUNCTIONAL_TEST_KMS_ARN"; @@ -36,8 +35,10 @@ mod tests { } lazy_static! { - static ref TMP_DIR: TempDir = - TempDir::new("sops-functional-tests").expect("Unable to create temporary directory"); + static ref TMP_DIR: TempDir = Builder::new() + .prefix("sops-functional-tests") + .tempdir() + .expect("Unable to create temporary directory"); } fn prepare_temp_file(name: &str, contents: &[u8]) -> String { @@ -80,6 +81,47 @@ mod tests { } } + fn write_to_stdin(process: &Child, content: &[u8]) { + let mut outstdin = process.stdin.as_ref().unwrap(); + let mut writer = BufWriter::new(&mut outstdin); + writer.write_all(content).expect("Cannot write to stdin"); + } + + #[test] + fn encrypt_from_stdin() { + let process = Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("--filename-override") + .arg("test_encrypt.yaml") + .arg("--output-type") + .arg("json") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin( + &process, + b"foo: 2 +bar: baz +", + ); + let output = process.wait_with_output().expect("Failed to wait on sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + let json = &String::from_utf8_lossy(&output.stdout); + let data: Value = serde_json::from_str(json).expect("Error parsing sops's JSON output"); + match data.into() { + Value::Mapping(m) => { + assert!( + m.get(&Value::String("sops".to_owned())).is_some(), + "sops metadata branch not found" + ); + assert_encrypted!(&m, Value::String("foo".to_owned())); + assert_encrypted!(&m, Value::String("bar".to_owned())); + } + _ => panic!("sops's JSON output is not an object"), + } + } + #[test] #[ignore] fn publish_json_file_s3() { @@ -295,6 +337,115 @@ bar: baz", panic!("Output JSON does not have the expected structure"); } + #[test] + fn set_json_file_update_idempotent_write() { + let file_path = prepare_temp_file( + "test_set_update_idempotent_write.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let mut before = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut before) + .unwrap(); + let output = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--output-type") + .arg("yaml") + .arg(file_path.clone()) + .arg(r#"["b"]"#) + .arg(r#""ba""#) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut after = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut after) + .unwrap(); + assert!(before != after); + assert!(after.starts_with("a: ")); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--input-type") + .arg("yaml") + .arg("--output-type") + .arg("yaml") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let data = &String::from_utf8_lossy(&output.stdout); + assert!(data == "a: 2\nb: ba\n"); + } + + #[test] + fn set_json_file_update_idempotent_nowrite() { + let file_path = prepare_temp_file( + "test_set_update_idempotent_nowrite.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let mut before = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut before) + .unwrap(); + let output = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--output-type") + .arg("yaml") + .arg("--idempotent") + .arg(file_path.clone()) + .arg(r#"["b"]"#) + .arg(r#""ba""#) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut after = String::new(); + File::open(file_path.clone()) + .unwrap() + .read_to_string(&mut after) + .unwrap(); + println!("before: {}\nafter: {}", &before, &after,); + assert!(before == after); + } + #[test] fn set_json_file_insert() { let file_path = @@ -549,11 +700,57 @@ b: ba"# } } + #[test] + fn test_yaml_time() { + let file_path = prepare_temp_file( + "test_time.yaml", + r#"a: 2024-01-01 +b: 2006-01-02T15:04:05+07:06"# + .as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut s = String::new(); + File::open(file_path) + .unwrap() + .read_to_string(&mut s) + .unwrap(); + assert_eq!( + s, + r#"a: 2024-01-01T00:00:00Z +b: 2006-01-02T15:04:05+07:06 +"# + ); + } + #[test] fn unset_json_file() { // Test removal of tree branch - let file_path = - prepare_temp_file("test_unset.json", r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes()); + let file_path = prepare_temp_file( + "test_unset.json", + r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes(), + ); assert!( Command::new(SOPS_BINARY_PATH) .arg("encrypt") @@ -661,8 +858,10 @@ b: ba"# #[test] fn unset_yaml_file() { // Test removal of tree branch - let file_path = - prepare_temp_file("test_unset.yaml", r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes()); + let file_path = prepare_temp_file( + "test_unset.yaml", + r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes(), + ); assert!( Command::new(SOPS_BINARY_PATH) .arg("encrypt") @@ -797,12 +996,42 @@ b: ba"# ); } + #[test] + fn decrypt_from_stdin() { + let process = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--input-type") + .arg("yaml") + .arg("--output-type") + .arg("yaml") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin(&process, include_bytes!("../res/comments.enc.yaml")); + let output = process.wait_with_output().expect("Failed to wait on sops"); + assert!(output.status.success(), "sops didn't exit successfully"); + let yaml = &String::from_utf8_lossy(&output.stdout); + let data: Value = serde_yaml::from_str(&yaml).expect("Error parsing sops's YAML output"); + match data.into() { + Value::Mapping(m) => { + assert!( + m.get(&Value::String("sops".to_owned())).is_none(), + "sops metadata branch found" + ); + assert_eq!(m["lorem"], Value::String("ipsum".to_owned())); + assert_eq!(m["dolor"], Value::String("sit".to_owned())); + } + _ => panic!("sops's JSON output is not an object"), + } + } + #[test] fn encrypt_comments() { let file_path = "res/comments.yaml"; let output = Command::new(SOPS_BINARY_PATH) .arg("encrypt") - .arg(file_path.clone()) + .arg(file_path) .output() .expect("Error running sops"); assert!(output.status.success(), "SOPS didn't return successfully"); @@ -821,7 +1050,7 @@ b: ba"# let file_path = "res/comments_list.yaml"; let output = Command::new(SOPS_BINARY_PATH) .arg("encrypt") - .arg(file_path.clone()) + .arg(file_path) .output() .expect("Error running sops"); assert!(output.status.success(), "SOPS didn't return successfully"); @@ -840,7 +1069,7 @@ b: ba"# let file_path = "res/comments.enc.yaml"; let output = Command::new(SOPS_BINARY_PATH) .arg("decrypt") - .arg(file_path.clone()) + .arg(file_path) .output() .expect("Error running sops"); assert!(output.status.success(), "SOPS didn't return successfully"); @@ -859,7 +1088,7 @@ b: ba"# let file_path = "res/comments_unencrypted_comments.yaml"; let output = Command::new(SOPS_BINARY_PATH) .arg("decrypt") - .arg(file_path.clone()) + .arg(file_path) .output() .expect("Error running sops"); assert!(output.status.success(), "SOPS didn't return successfully"); @@ -945,6 +1174,66 @@ b: ba"# ); } + #[test] + fn test_no_keygroups() { + // The .sops.yaml file ensures this file is encrypted by zero keygroups + let file_path = prepare_temp_file("test_no_keygroups.yaml", "a: secret".as_bytes()); + let output = Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!( + !output.status.success(), + "SOPS succeeded encrypting a file without a key group" + ); + assert_eq!( + std::str::from_utf8(&output.stderr).unwrap(), + "Could not generate data key: [empty key group provided]\n" + ); + } + + #[test] + fn test_zero_keygroups() { + // The .sops.yaml file ensures this file is encrypted by zero keygroups + let file_path = prepare_temp_file("test_zero_keygroups.yaml", "a: secret".as_bytes()); + let output = Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!( + !output.status.success(), + "SOPS succeeded encrypting a file without a key group" + ); + assert_eq!( + std::str::from_utf8(&output.stderr).unwrap(), + "Could not generate data key: [empty key group provided]\n" + ); + } + + #[test] + fn test_empty_keygroup() { + // The .sops.yaml file ensures this file is encrypted by zero keygroups + let file_path = prepare_temp_file("test_empty_keygroup.yaml", "a: secret".as_bytes()); + let output = Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!( + !output.status.success(), + "SOPS succeeded encrypting a file without a key group" + ); + assert_eq!( + std::str::from_utf8(&output.stderr).unwrap(), + "Could not generate data key: [empty key group provided]\n" + ); + } + #[test] fn extract_string() { let file_path = prepare_temp_file( @@ -1164,7 +1453,8 @@ bar: |- r#"{ "foo": "bar", "bar": "baz\nbam" -}"# +} +"# ); } diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 8ff51357d..7c79e24aa 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -12,6 +12,7 @@ import ( kms "cloud.google.com/go/kms/apiv1" "cloud.google.com/go/kms/apiv1/kmspb" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" "google.golang.org/api/option" "google.golang.org/grpc" @@ -50,6 +51,11 @@ type MasterKey struct { // for NeedsRotation. CreationDate time.Time + // tokenSource contains the oauth2.TokenSource used by the GCP client. + // It can be injected by a (local) keyservice.KeyServiceServer using + // TokenSource.ApplyToMasterKey. + // If nil, the remaining authentication methods are attempted. + tokenSource oauth2.TokenSource // credentialJSON is the Service Account credentials JSON used for // authenticating towards the GCP KMS service. credentialJSON []byte @@ -82,6 +88,22 @@ func MasterKeysFromResourceIDString(resourceID string) []*MasterKey { return keys } +// TokenSource is an oauth2.TokenSource used for authenticating towards the +// GCP KMS service. +type TokenSource struct { + source oauth2.TokenSource +} + +// NewTokenSource creates a new TokenSource from the provided oauth2.TokenSource. +func NewTokenSource(source oauth2.TokenSource) TokenSource { + return TokenSource{source: source} +} + +// ApplyToMasterKey configures the TokenSource on the provided key. +func (t TokenSource) ApplyToMasterKey(key *MasterKey) { + key.tokenSource = t.source +} + // CredentialJSON is the Service Account credentials JSON used for authenticating // towards the GCP KMS service. type CredentialJSON []byte @@ -203,8 +225,8 @@ func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } -// newKMSClient returns a GCP KMS client configured with the credentialJSON -// and/or grpcConn, falling back to environmental defaults. +// newKMSClient returns a GCP KMS client configured with the tokenSource +// or credentialJSON, and/or grpcConn, falling back to environmental defaults. // It returns an error if the ResourceID is invalid, or if the setup of the // client fails. func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { @@ -216,6 +238,8 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { var opts []option.ClientOption switch { + case key.tokenSource != nil: + opts = append(opts, option.WithTokenSource(key.tokenSource)) case key.credentialJSON != nil: opts = append(opts, option.WithCredentialsJSON(key.credentialJSON)) default: diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index 153bfb260..b9e3b243c 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -9,6 +9,7 @@ import ( "cloud.google.com/go/kms/apiv1/kmspb" "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -38,6 +39,13 @@ func TestMasterKeysFromResourceIDString(t *testing.T) { } } +func TestTokenSource_ApplyToMasterKey(t *testing.T) { + src := NewTokenSource(oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "some-token"})) + key := &MasterKey{} + src.ApplyToMasterKey(key) + assert.Equal(t, src.source, key.tokenSource) +} + func TestCredentialJSON_ApplyToMasterKey(t *testing.T) { key := &MasterKey{} credential := CredentialJSON("mock") @@ -159,7 +167,7 @@ func newGRPCServer(port string) *grpc.ClientConn { } go serv.Serve(lis) - conn, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index d7c5be65f..a400cee3c 100644 --- a/go.mod +++ b/go.mod @@ -1,135 +1,148 @@ module github.com/getsops/sops/v3 -go 1.21 +go 1.22 +toolchain go1.24.1 require ( - cloud.google.com/go/kms v1.18.4 - cloud.google.com/go/storage v1.43.0 - filippo.io/age v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 - github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton - github.com/aws/aws-sdk-go-v2 v1.30.3 - github.com/aws/aws-sdk-go-v2/config v1.27.27 - github.com/aws/aws-sdk-go-v2/credentials v1.17.27 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 - github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 - github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 + cloud.google.com/go/kms v1.21.1 + cloud.google.com/go/storage v1.51.0 + filippo.io/age v1.2.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 + github.com/ProtonMail/go-crypto v1.1.6 + github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/config v1.29.9 + github.com/aws/aws-sdk-go-v2/credentials v1.17.62 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 + github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 github.com/blang/semver v3.5.1+incompatible - github.com/fatih/color v1.17.0 - github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe - github.com/golang/protobuf v1.5.4 - github.com/google/go-cmp v0.6.0 + github.com/fatih/color v1.18.0 + github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e + github.com/google/go-cmp v0.7.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/goware/prefixer v0.0.0-20160118172347-395022866408 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/vault/api v1.14.0 + github.com/hashicorp/vault/api v1.16.0 github.com/lib/pq v1.10.9 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 - github.com/ory/dockertest/v3 v3.10.0 + github.com/ory/dockertest/v3 v3.11.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.9.0 - github.com/urfave/cli v1.22.15 - golang.org/x/net v0.27.0 - golang.org/x/sys v0.22.0 - golang.org/x/term v0.22.0 - google.golang.org/api v0.189.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade - google.golang.org/grpc v1.65.0 - google.golang.org/protobuf v1.34.2 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli v1.22.16 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.37.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sys v0.31.0 + golang.org/x/term v0.30.0 + google.golang.org/api v0.227.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.5 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - cloud.google.com/go v0.115.0 // indirect - cloud.google.com/go/auth v0.7.2 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect - cloud.google.com/go/iam v1.1.10 // indirect - cloud.google.com/go/longrunning v0.5.9 // indirect - dario.cat/mergo v1.0.0 // indirect + cel.dev/expr v0.19.2 // indirect + cloud.google.com/go v0.118.3 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.4.1 // indirect + cloud.google.com/go/longrunning v0.6.5 // indirect + cloud.google.com/go/monitoring v1.24.0 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect - github.com/aws/smithy-go v1.20.3 // indirect - github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudflare/circl v1.3.9 // indirect - github.com/containerd/continuity v0.4.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.5.0 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/containerd/continuity v0.4.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v27.0.1+incompatible // indirect - github.com/docker/docker v27.1.0+incompatible // indirect + github.com/docker/cli v27.4.1+incompatible // indirect + github.com/docker/docker v27.4.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.6 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.13 // indirect + github.com/opencontainers/runc v1.2.3 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 873781804..f396e8413 100644 --- a/go.sum +++ b/go.sum @@ -1,180 +1,185 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= -cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= -cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= -cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= -cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI= -cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps= -cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk= -cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g= -cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k= -cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= -cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= -cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE= -filippo.io/age v1.2.0/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= +cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= +cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= +cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= +cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk= +cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= +cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= +cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= +cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= +cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= +filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 h1:9fXQS/0TtQmKXp8SureKouF+idbQvp7cPUxykiohnBs= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1/go.mod h1:f+OaoSg0VQYPMqB0Jp2D54j1VHzITYcJaCNwV+k00ts= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton h1:KVBEgU3CJpmzLChnLiSuEyCuhGhcMt3eOST+7A+ckto= -github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= -github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= -github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= -github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 h1:TC2vjvaAv1VNl9A0rm+SeuBjrzXnrlwk6Yop+gKRi38= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= -github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= -github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= +github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 h1:MTLivtC3s89de7Fe3P8rzML/8XPNRfuyJhlRTsCEt0k= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66/go.mod h1:NAuQ2s6gaFEsuTIb2+P5t6amB1w5MhvJFxppoezGWH0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= -github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= +github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v27.0.1+incompatible h1:d/OrlblkOTkhJ1IaAGD1bLgUBtFQC/oP0VjkFMIN+B0= -github.com/docker/cli v27.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs= -github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= +github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= +github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe h1:QKe/kmAYbndxwu91TcjHERsnMh5SgOB1x/qicvOdUJ8= -github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -194,12 +199,14 @@ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSY github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= -github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU= -github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -223,25 +230,30 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= -github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -256,12 +268,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= -github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -271,53 +283,46 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -326,21 +331,17 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -348,37 +349,18 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI= -google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg= -google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= -google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY= -google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= +google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -389,7 +371,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= -gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/hcvault/keysource_test.go b/hcvault/keysource_test.go index 355a47868..646895335 100644 --- a/hcvault/keysource_test.go +++ b/hcvault/keysource_test.go @@ -41,7 +41,7 @@ func TestMain(m *testing.M) { } // Pull the image, create a container based on it, and run it - resource, err := pool.Run("vault", testVaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=" + testVaultToken}) + resource, err := pool.Run("ghcr.io/getsops/ci-container-images/vault", testVaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=" + testVaultToken}) if err != nil { logger.Fatalf("could not start resource: %s", err) } diff --git a/keyservice/keyservice.pb.go b/keyservice/keyservice.pb.go index ead3ccfd1..a810b2805 100644 --- a/keyservice/keyservice.pb.go +++ b/keyservice/keyservice.pb.go @@ -1,17 +1,12 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 -// protoc v3.13.0 +// protoc-gen-go v1.35.2 +// protoc v5.28.3 // source: keyservice/keyservice.proto package keyservice import ( - context "context" - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -25,16 +20,13 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - type Key struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to KeyType: + // // *Key_KmsKey // *Key_PgpKey // *Key_GcpKmsKey @@ -46,11 +38,9 @@ type Key struct { func (x *Key) Reset() { *x = Key{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Key) String() string { @@ -61,7 +51,7 @@ func (*Key) ProtoMessage() {} func (x *Key) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -175,11 +165,9 @@ type PgpKey struct { func (x *PgpKey) Reset() { *x = PgpKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PgpKey) String() string { @@ -190,7 +178,7 @@ func (*PgpKey) ProtoMessage() {} func (x *PgpKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -225,11 +213,9 @@ type KmsKey struct { func (x *KmsKey) Reset() { *x = KmsKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *KmsKey) String() string { @@ -240,7 +226,7 @@ func (*KmsKey) ProtoMessage() {} func (x *KmsKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -293,11 +279,9 @@ type GcpKmsKey struct { func (x *GcpKmsKey) Reset() { *x = GcpKmsKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GcpKmsKey) String() string { @@ -308,7 +292,7 @@ func (*GcpKmsKey) ProtoMessage() {} func (x *GcpKmsKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -342,11 +326,9 @@ type VaultKey struct { func (x *VaultKey) Reset() { *x = VaultKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VaultKey) String() string { @@ -357,7 +339,7 @@ func (*VaultKey) ProtoMessage() {} func (x *VaultKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -405,11 +387,9 @@ type AzureKeyVaultKey struct { func (x *AzureKeyVaultKey) Reset() { *x = AzureKeyVaultKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AzureKeyVaultKey) String() string { @@ -420,7 +400,7 @@ func (*AzureKeyVaultKey) ProtoMessage() {} func (x *AzureKeyVaultKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -466,11 +446,9 @@ type AgeKey struct { func (x *AgeKey) Reset() { *x = AgeKey{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AgeKey) String() string { @@ -481,7 +459,7 @@ func (*AgeKey) ProtoMessage() {} func (x *AgeKey) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -514,11 +492,9 @@ type EncryptRequest struct { func (x *EncryptRequest) Reset() { *x = EncryptRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EncryptRequest) String() string { @@ -529,7 +505,7 @@ func (*EncryptRequest) ProtoMessage() {} func (x *EncryptRequest) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -568,11 +544,9 @@ type EncryptResponse struct { func (x *EncryptResponse) Reset() { *x = EncryptResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EncryptResponse) String() string { @@ -583,7 +557,7 @@ func (*EncryptResponse) ProtoMessage() {} func (x *EncryptResponse) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -616,11 +590,9 @@ type DecryptRequest struct { func (x *DecryptRequest) Reset() { *x = DecryptRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DecryptRequest) String() string { @@ -631,7 +603,7 @@ func (*DecryptRequest) ProtoMessage() {} func (x *DecryptRequest) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -670,11 +642,9 @@ type DecryptResponse struct { func (x *DecryptResponse) Reset() { *x = DecryptResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_keyservice_keyservice_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_keyservice_keyservice_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DecryptResponse) String() string { @@ -685,7 +655,7 @@ func (*DecryptResponse) ProtoMessage() {} func (x *DecryptResponse) ProtoReflect() protoreflect.Message { mi := &file_keyservice_keyservice_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -784,7 +754,8 @@ var file_keyservice_keyservice_proto_rawDesc = []byte{ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2f, 0x6b, 0x65, + 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -800,7 +771,7 @@ func file_keyservice_keyservice_proto_rawDescGZIP() []byte { } var file_keyservice_keyservice_proto_msgTypes = make([]protoimpl.MessageInfo, 12) -var file_keyservice_keyservice_proto_goTypes = []interface{}{ +var file_keyservice_keyservice_proto_goTypes = []any{ (*Key)(nil), // 0: Key (*PgpKey)(nil), // 1: PgpKey (*KmsKey)(nil), // 2: KmsKey @@ -840,141 +811,7 @@ func file_keyservice_keyservice_proto_init() { if File_keyservice_keyservice_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_keyservice_keyservice_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Key); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PgpKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*KmsKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GcpKmsKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VaultKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AzureKeyVaultKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AgeKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EncryptRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EncryptResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DecryptRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_keyservice_keyservice_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DecryptResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_keyservice_keyservice_proto_msgTypes[0].OneofWrappers = []interface{}{ + file_keyservice_keyservice_proto_msgTypes[0].OneofWrappers = []any{ (*Key_KmsKey)(nil), (*Key_PgpKey)(nil), (*Key_GcpKmsKey)(nil), @@ -1001,119 +838,3 @@ func file_keyservice_keyservice_proto_init() { file_keyservice_keyservice_proto_goTypes = nil file_keyservice_keyservice_proto_depIdxs = nil } - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConnInterface - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion6 - -// KeyServiceClient is the client API for KeyService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type KeyServiceClient interface { - Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) - Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) -} - -type keyServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewKeyServiceClient(cc grpc.ClientConnInterface) KeyServiceClient { - return &keyServiceClient{cc} -} - -func (c *keyServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) { - out := new(EncryptResponse) - err := c.cc.Invoke(ctx, "/KeyService/Encrypt", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *keyServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) { - out := new(DecryptResponse) - err := c.cc.Invoke(ctx, "/KeyService/Decrypt", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// KeyServiceServer is the server API for KeyService service. -type KeyServiceServer interface { - Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) - Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) -} - -// UnimplementedKeyServiceServer can be embedded to have forward compatible implementations. -type UnimplementedKeyServiceServer struct { -} - -func (*UnimplementedKeyServiceServer) Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented") -} -func (*UnimplementedKeyServiceServer) Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented") -} - -func RegisterKeyServiceServer(s *grpc.Server, srv KeyServiceServer) { - s.RegisterService(&_KeyService_serviceDesc, srv) -} - -func _KeyService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(EncryptRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(KeyServiceServer).Encrypt(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/KeyService/Encrypt", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(KeyServiceServer).Encrypt(ctx, req.(*EncryptRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _KeyService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DecryptRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(KeyServiceServer).Decrypt(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/KeyService/Decrypt", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(KeyServiceServer).Decrypt(ctx, req.(*DecryptRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _KeyService_serviceDesc = grpc.ServiceDesc{ - ServiceName: "KeyService", - HandlerType: (*KeyServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Encrypt", - Handler: _KeyService_Encrypt_Handler, - }, - { - MethodName: "Decrypt", - Handler: _KeyService_Decrypt_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "keyservice/keyservice.proto", -} diff --git a/keyservice/keyservice.proto b/keyservice/keyservice.proto index 1d91a5709..8bf62f89b 100644 --- a/keyservice/keyservice.proto +++ b/keyservice/keyservice.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +option go_package = "./keyservice"; + message Key { oneof key_type { KmsKey kms_key = 1; diff --git a/keyservice/keyservice_grpc.pb.go b/keyservice/keyservice_grpc.pb.go new file mode 100644 index 000000000..d278b82d9 --- /dev/null +++ b/keyservice/keyservice_grpc.pb.go @@ -0,0 +1,157 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.3 +// source: keyservice/keyservice.proto + +package keyservice + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + KeyService_Encrypt_FullMethodName = "/KeyService/Encrypt" + KeyService_Decrypt_FullMethodName = "/KeyService/Decrypt" +) + +// KeyServiceClient is the client API for KeyService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type KeyServiceClient interface { + Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) + Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) +} + +type keyServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewKeyServiceClient(cc grpc.ClientConnInterface) KeyServiceClient { + return &keyServiceClient{cc} +} + +func (c *keyServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EncryptResponse) + err := c.cc.Invoke(ctx, KeyService_Encrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *keyServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DecryptResponse) + err := c.cc.Invoke(ctx, KeyService_Decrypt_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// KeyServiceServer is the server API for KeyService service. +// All implementations should embed UnimplementedKeyServiceServer +// for forward compatibility. +type KeyServiceServer interface { + Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) + Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) +} + +// UnimplementedKeyServiceServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedKeyServiceServer struct{} + +func (UnimplementedKeyServiceServer) Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented") +} +func (UnimplementedKeyServiceServer) Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented") +} +func (UnimplementedKeyServiceServer) testEmbeddedByValue() {} + +// UnsafeKeyServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to KeyServiceServer will +// result in compilation errors. +type UnsafeKeyServiceServer interface { + mustEmbedUnimplementedKeyServiceServer() +} + +func RegisterKeyServiceServer(s grpc.ServiceRegistrar, srv KeyServiceServer) { + // If the following call pancis, it indicates UnimplementedKeyServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&KeyService_ServiceDesc, srv) +} + +func _KeyService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeyServiceServer).Encrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KeyService_Encrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeyServiceServer).Encrypt(ctx, req.(*EncryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KeyService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DecryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KeyServiceServer).Decrypt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KeyService_Decrypt_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KeyServiceServer).Decrypt(ctx, req.(*DecryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// KeyService_ServiceDesc is the grpc.ServiceDesc for KeyService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var KeyService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "KeyService", + HandlerType: (*KeyServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Encrypt", + Handler: _KeyService_Encrypt_Handler, + }, + { + MethodName: "Decrypt", + Handler: _KeyService_Decrypt_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "keyservice/keyservice.proto", +} diff --git a/kms/keysource.go b/kms/keysource.go index e1441b492..d3be8d104 100644 --- a/kms/keysource.go +++ b/kms/keysource.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "regexp" + "sort" "strings" "time" @@ -181,6 +182,38 @@ func ParseKMSContext(in interface{}) map[string]*string { return out } +// kmsContextToString converts a dictionary into a string that can be parsed +// again with ParseKMSContext(). +func kmsContextToString(in map[string]*string) string { + if len(in) == 0 { + return "" + } + + // Collect the keys in a slice and compute the expected length + keys := make([]string, 0, len(in)) + length := 0 + for key := range in { + keys = append(keys, key) + length += len(key) + len(*in[key]) + 2 + } + + // Sort the keys + sort.Strings(keys) + + // Compose a comma-separated string of key-vale pairs + var builder strings.Builder + builder.Grow(length) + for index, key := range keys { + if index > 0 { + builder.WriteString(",") + } + builder.WriteString(key) + builder.WriteByte(':') + builder.WriteString(*in[key]) + } + return builder.String() +} + // CredentialsProvider is a wrapper around aws.CredentialsProvider used for // authentication towards AWS KMS. type CredentialsProvider struct { @@ -278,7 +311,18 @@ func (key *MasterKey) NeedsRotation() bool { // ToString converts the key to a string representation. func (key *MasterKey) ToString() string { - return key.Arn + arnRole := key.Arn + if key.Role != "" { + arnRole = fmt.Sprintf("%s+%s", key.Arn, key.Role) + } + context := kmsContextToString(key.EncryptionContext) + if key.AwsProfile != "" { + return fmt.Sprintf("%s|%s|%s", arnRole, context, key.AwsProfile) + } + if context != "" { + return fmt.Sprintf("%s|%s", arnRole, context) + } + return arnRole } // ToMap converts the MasterKey to a map for serialization purposes. diff --git a/kms/keysource_test.go b/kms/keysource_test.go index a2bb76b3f..da3c6b51e 100644 --- a/kms/keysource_test.go +++ b/kms/keysource_test.go @@ -33,7 +33,7 @@ const ( // testLocalKMSImage is a container image repository reference to a mock // version of AWS' Key Management Service. // Ref: https://github.com/nsmithuk/local-kms - testLocalKMSImage = "docker.io/nsmithuk/local-kms" + testLocalKMSImage = "ghcr.io/getsops/ci-container-images/local-kms" // testLocalKMSImage is the container image tag to use. testLocalKMSTag = "3.11.1" ) @@ -367,8 +367,38 @@ func TestMasterKey_NeedsRotation(t *testing.T) { } func TestMasterKey_ToString(t *testing.T) { + dummyARNWithRole := fmt.Sprintf("%s+arn:aws:iam::my-role", dummyARN) + + bar := "bar" + bam := "bam" + context := map[string]*string{ + "foo": &bar, + "baz": &bam, + } + key := NewMasterKeyFromArn(dummyARN, nil, "") assert.Equal(t, dummyARN, key.ToString()) + + key = NewMasterKeyFromArn(dummyARNWithRole, nil, "") + assert.Equal(t, dummyARNWithRole, key.ToString()) + + key = NewMasterKeyFromArn(dummyARN, nil, "profile") + assert.Equal(t, fmt.Sprintf("%s||profile", dummyARN), key.ToString()) + + key = NewMasterKeyFromArn(dummyARNWithRole, nil, "profile") + assert.Equal(t, fmt.Sprintf("%s||profile", dummyARNWithRole), key.ToString()) + + key = NewMasterKeyFromArn(dummyARN, context, "") + assert.Equal(t, fmt.Sprintf("%s|baz:bam,foo:bar", dummyARN), key.ToString()) + + key = NewMasterKeyFromArn(dummyARNWithRole, context, "") + assert.Equal(t, fmt.Sprintf("%s|baz:bam,foo:bar", dummyARNWithRole), key.ToString()) + + key = NewMasterKeyFromArn(dummyARN, context, "profile") + assert.Equal(t, fmt.Sprintf("%s|baz:bam,foo:bar|profile", dummyARN), key.ToString()) + + key = NewMasterKeyFromArn(dummyARNWithRole, context, "profile") + assert.Equal(t, fmt.Sprintf("%s|baz:bam,foo:bar|profile", dummyARNWithRole), key.ToString()) } func TestMasterKey_ToMap(t *testing.T) { diff --git a/pgp/keysource.go b/pgp/keysource.go index e1eed580d..65769a3bf 100644 --- a/pgp/keysource.go +++ b/pgp/keysource.go @@ -428,7 +428,17 @@ func (key *MasterKey) decryptWithGnuPG() ([]byte, error) { return nil, fmt.Errorf("failed to decrypt sops data key with pgp: %s", strings.TrimSpace(stderr.String())) } - return stdout.Bytes(), nil + result := stdout.Bytes() + if len(result) == 0 { + // This can happen if an older GnuPG version is used to decrypt a key encrypted with a + // newer GnuPG version that used an AEAD cipher, which the old version does not support. + // Apparently some GnuPG versions drop the unspuported packets, which results in a decrypted + // data of 0 bytes, and returns nothing with exit code 0. + // + // (See https://github.com/getsops/sops/issues/896#issuecomment-2688079300 for more infos.) + return nil, fmt.Errorf("failed to decrypt sops data key with pgp: zero bytes returned") + } + return result, nil } // NeedsRotation returns whether the data key needs to be rotated @@ -634,7 +644,13 @@ func gnuPGHome(customPath string) string { // This is mostly used for compatibility reasons, as older versions of GnuPG // do not always like long IDs. func shortenFingerprint(fingerprint string) string { - if offset := len(fingerprint) - 16; offset > 0 { + offset := len(fingerprint) - 16 + // If the fingerprint ends with '!', we must include '!' in the ID *and* the + // 16 hex digits before it. See https://github.com/getsops/sops/issues/1365. + if strings.HasSuffix(fingerprint, "!") { + offset -= 1 + } + if offset > 0 { fingerprint = fingerprint[offset:] } return fingerprint diff --git a/pgp/keysource_test.go b/pgp/keysource_test.go index 58693d72d..28fcfeb8e 100644 --- a/pgp/keysource_test.go +++ b/pgp/keysource_test.go @@ -697,10 +697,23 @@ func Test_gnuPGHome(t *testing.T) { } func Test_shortenFingerprint(t *testing.T) { + // Test with regular fingerprint shortId := shortenFingerprint(mockFingerprint) assert.Equal(t, "9732075EA221A7EA", shortId) assert.Equal(t, shortId, shortenFingerprint(shortId)) + + // Test with forced subkey + shortId = shortenFingerprint(mockFingerprint + "!") + assert.Equal(t, "9732075EA221A7EA!", shortId) + + assert.Equal(t, shortId, shortenFingerprint(shortId)) + + // Make sure that too short IDs are kept + for _, tooShort := range []string{"012345679abcdef", "012345679abcdef!", "123", "123!"} { + shortId = shortenFingerprint(tooShort) + assert.Equal(t, tooShort, shortId) + } } // TODO(hidde): previous tests kept around for now. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..3a445918b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.85.0" +profile = "minimal" diff --git a/shamir/shamir.go b/shamir/shamir.go index 10d7bc3ba..b3f4f1d50 100644 --- a/shamir/shamir.go +++ b/shamir/shamir.go @@ -15,7 +15,6 @@ import ( "crypto/subtle" "fmt" mathrand "math/rand" - "time" ) const ( @@ -190,7 +189,6 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { // a non-cryptographically secure source of randomness is used. // As far as I know the x coordinates do not need to be random. - mathrand.Seed(time.Now().UnixNano()) xCoordinates := mathrand.Perm(255) // Allocate the output array, initialize the final byte diff --git a/sops.go b/sops.go index 4b97292a1..a32211f1a 100644 --- a/sops.go +++ b/sops.go @@ -127,6 +127,48 @@ type TreeBranch []TreeItem // Trees usually have more than one branch type TreeBranches []TreeBranch +func equals(oneBranch interface{}, otherBranch interface{}) bool { + switch oneBranch := oneBranch.(type) { + case TreeBranch: + otherBranch, ok := otherBranch.(TreeBranch) + if !ok || len(oneBranch) != len(otherBranch) { + return false + } + for i, item := range oneBranch { + otherItem := otherBranch[i] + if !equals(item.Key, otherItem.Key) || !equals(item.Value, otherItem.Value) { + return false + } + } + return true + case []interface{}: + otherBranch, ok := otherBranch.([]interface{}) + if !ok || len(oneBranch) != len(otherBranch) { + return false + } + for i, item := range oneBranch { + if !equals(item, otherBranch[i]) { + return false + } + } + return true + case Comment: + otherBranch, ok := otherBranch.(Comment) + if !ok { + return false + } + return oneBranch.Value == otherBranch.Value + default: + // Unexpected type + return oneBranch == otherBranch + } +} + +// Compare a branch with another one +func (branch TreeBranch) Equals(other TreeBranch) bool { + return equals(branch, other) +} + func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} { switch component := path[0].(type) { case int: @@ -156,47 +198,55 @@ func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} { } } -func set(branch interface{}, path []interface{}, value interface{}) interface{} { +func set(branch interface{}, path []interface{}, value interface{}) (interface{}, bool) { switch branch := branch.(type) { case TreeBranch: for i, item := range branch { if item.Key == path[0] { + var changed bool if len(path) == 1 { + changed = !equals(branch[i].Value, value) branch[i].Value = value } else { - branch[i].Value = set(item.Value, path[1:], value) + branch[i].Value, changed = set(item.Value, path[1:], value) } - return branch + return branch, changed } } // Not found, need to add the next path entry to the branch value := valueFromPathAndLeaf(path, value) if newBranch, ok := value.(TreeBranch); ok && len(newBranch) > 0 { - return append(branch, newBranch[0]) + return append(branch, newBranch[0]), true } - return branch + return branch, true case []interface{}: position := path[0].(int) + var changed bool if len(path) == 1 { if position >= len(branch) { - return append(branch, value) + return append(branch, value), true } + changed = !equals(branch[position], value) branch[position] = value } else { if position >= len(branch) { branch = append(branch, valueFromPathAndLeaf(path[1:], value)) + changed = true + } else { + branch[position], changed = set(branch[position], path[1:], value) } - branch[position] = set(branch[position], path[1:], value) } - return branch + return branch, changed default: - return valueFromPathAndLeaf(path, value) + newValue := valueFromPathAndLeaf(path, value) + return newValue, !equals(branch, newValue) } } // Set sets a value on a given tree for the specified path -func (branch TreeBranch) Set(path []interface{}, value interface{}) TreeBranch { - return set(branch, path, value).(TreeBranch) +func (branch TreeBranch) Set(path []interface{}, value interface{}) (TreeBranch, bool) { + v, changed := set(branch, path, value) + return v.(TreeBranch), changed } func unset(branch interface{}, path []interface{}) (interface{}, error) { @@ -297,6 +347,8 @@ func (branch TreeBranch) walkValue(in interface{}, path []string, commentsStack return onLeaves(in, path, commentsStack) case float64: return onLeaves(in, path, commentsStack) + case time.Time: + return onLeaves(in, path, commentsStack) case Comment: return onLeaves(in, path, commentsStack) case TreeBranch: @@ -700,6 +752,11 @@ func (m *Metadata) UpdateMasterKeysWithKeyServices(dataKey []byte, svcs []keyser fmt.Errorf("no key services provided, cannot update master keys"), } } + if len(m.KeyGroups) == 0 { + return []error{ + fmt.Errorf("no key groups provided"), + } + } var parts [][]byte if len(m.KeyGroups) == 1 { // If there's only one key group, we can't do Shamir. All keys @@ -726,6 +783,11 @@ func (m *Metadata) UpdateMasterKeysWithKeyServices(dataKey []byte, svcs []keyser } for i, group := range m.KeyGroups { part := parts[i] + if len(group) == 0 { + return []error{ + fmt.Errorf("empty key group provided"), + } + } for _, key := range group { svcKey := keyservice.KeyFromMasterKey(key) var keyErrs []error @@ -762,7 +824,7 @@ func (m *Metadata) UpdateMasterKeys(dataKey []byte) (errs []error) { // GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each // MasterKey in the Metadata's KeySources until one of them succeeds. -func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) { +func (m *Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) { if m.DataKey != nil { return m.DataKey, nil } @@ -908,6 +970,8 @@ func ToBytes(in interface{}) ([]byte, error) { return boolB, nil case []byte: return in, nil + case time.Time: + return in.MarshalText() case Comment: return ToBytes(in.Value) default: diff --git a/sops_test.go b/sops_test.go index 4a58f0866..0115024a0 100644 --- a/sops_test.go +++ b/sops_test.go @@ -633,8 +633,7 @@ func TestUnencryptedCommentRegexFail(t *testing.T) { tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedCommentRegex: "ENC"}} cipher := encPrefixCipher{} _, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher) - assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Encrypted comment \"ENC:sops:noenc\" matches UnencryptedCommentRegex!") + assert.ErrorContains(t, err, "Encrypted comment \"ENC:sops:noenc\" matches UnencryptedCommentRegex!") } type MockCipher struct{} @@ -845,6 +844,23 @@ func TestDecrypt(t *testing.T) { } } +type WrongType struct{} + +func TestEncryptWrongType(t *testing.T) { + branches := TreeBranches{ + TreeBranch{ + TreeItem{ + Key: "foo", + Value: WrongType{}, + }, + }, + } + tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedSuffix: DefaultUnencryptedSuffix}} + result, err := tree.Encrypt(bytes.Repeat([]byte{'f'}, 32), MockCipher{}) + assert.Equal(t, "", result) + assert.ErrorContains(t, err, "unknown type: sops.WrongType") +} + func TestTruncateTree(t *testing.T) { tree := TreeBranch{ TreeItem{ @@ -872,10 +888,49 @@ func TestTruncateTree(t *testing.T) { "foobar", 2, }) - assert.Equal(t, nil, err) + assert.NoError(t, err) assert.Equal(t, expected, result) } +func TestTruncateTreeNotFound(t *testing.T) { + tree := TreeBranch{ + TreeItem{ + Key: "foo", + Value: 2, + }, + } + result, err := tree.Truncate([]interface{}{"baz"}) + assert.ErrorContains(t, err, "baz") + assert.Nil(t, result, "Truncate result was not nil upon %s", err) +} + +func TestTruncateTreeNotArray(t *testing.T) { + tree := TreeBranch{ + TreeItem{ + Key: "foo", + Value: 2, + }, + } + result, err := tree.Truncate([]interface{}{"foo", 99}) + assert.ErrorContains(t, err, "99") + assert.Nil(t, result, "Truncate result was not nil upon %s", err) +} + +func TestTruncateTreeArrayOutOfBounds(t *testing.T) { + tree := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + "one", + "two", + }, + }, + } + result, err := tree.Truncate([]interface{}{"foo", 99}) + assert.ErrorContains(t, err, "99") + assert.Nil(t, result, "Truncate result was not nil upon %s", err) +} + func TestEncryptComments(t *testing.T) { tree := Tree{ Branches: TreeBranches{ @@ -968,10 +1023,33 @@ func TestSetNewKey(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", "bar", "foo"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "foo"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[1].Value) } +func TestSetNewKeyUnchanged(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "baz", + Value: "foobar", + }, + }, + }, + }, + }, + } + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "foobar") + assert.Equal(t, false, changed) + assert.Equal(t, "foobar", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) +} + func TestSetNewBranch(t *testing.T) { branch := TreeBranch{ TreeItem{ @@ -979,7 +1057,8 @@ func TestSetNewBranch(t *testing.T) { Value: "value", }, } - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "key", @@ -1012,7 +1091,8 @@ func TestSetArrayDeepNew(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", 2, "bar"}, "hello") + set, changed := branch.Set([]interface{}{"foo", 2, "bar"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.([]interface{})[2].(TreeBranch)[0].Value) } @@ -1023,13 +1103,15 @@ func TestSetNewKeyDeep(t *testing.T) { Value: "bar", }, } - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) } func TestSetNewKeyOnEmptyBranch(t *testing.T) { branch := TreeBranch{} - set := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + set, changed := branch.Set([]interface{}{"foo", "bar", "baz"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, "hello", set[0].Value.(TreeBranch)[0].Value.(TreeBranch)[0].Value) } @@ -1044,13 +1126,15 @@ func TestSetArray(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"foo", 0}, "uno") + set, changed := branch.Set([]interface{}{"foo", 0}, "uno") + assert.Equal(t, true, changed) assert.Equal(t, "uno", set[0].Value.([]interface{})[0]) } func TestSetArrayNew(t *testing.T) { branch := TreeBranch{} - set := branch.Set([]interface{}{"foo", 0, 0}, "uno") + set, changed := branch.Set([]interface{}{"foo", 0, 0}, "uno") + assert.Equal(t, true, changed) assert.Equal(t, "uno", set[0].Value.([]interface{})[0].([]interface{})[0]) } @@ -1061,7 +1145,8 @@ func TestSetExisting(t *testing.T) { Value: "foobar", }, } - set := branch.Set([]interface{}{"foo"}, "bar") + set, changed := branch.Set([]interface{}{"foo"}, "bar") + assert.Equal(t, true, changed) assert.Equal(t, "bar", set[0].Value) } @@ -1072,7 +1157,8 @@ func TestSetArrayLeafNewItem(t *testing.T) { Value: []interface{}{}, }, } - set := branch.Set([]interface{}{"array", 2}, "hello") + set, changed := branch.Set([]interface{}{"array", 2}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "array", @@ -1092,7 +1178,8 @@ func TestSetArrayNonLeaf(t *testing.T) { }, }, } - set := branch.Set([]interface{}{"array", 0, "hello"}, "hello") + set, changed := branch.Set([]interface{}{"array", 0, "hello"}, "hello") + assert.Equal(t, true, changed) assert.Equal(t, TreeBranch{ TreeItem{ Key: "array", @@ -1108,6 +1195,315 @@ func TestSetArrayNonLeaf(t *testing.T) { }, set) } +func TestUnsetKeyRootLeaf(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: "foo", + }, + TreeItem{ + Key: "foofoo", + Value: "foofoo", + }, + } + unset, err := branch.Unset([]interface{}{"foofoo"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: "foo", + }, + }, unset) +} + +func TestUnsetKeyBranchLeaf(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + TreeItem{ + Key: "barbar", + Value: "barbar", + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", "barbar"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, unset) +} + +func TestUnsetKeyBranch(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: "foo", + }, + TreeItem{ + Key: "foofoo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foofoo"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: "foo", + }, + }, unset) +} + +func TestUnsetKeyRootLastLeaf(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: "foo", + }, + } + unset, err := branch.Unset([]interface{}{"foo"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + }, unset) +} + +func TestUnsetKeyBranchLastLeaf(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", "bar"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + }, + }, + }, unset) +} + +func TestUnsetKeyArray(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "baz", + Value: "baz", + }, + }, + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", "bar"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + }, + }, + }, unset) +} + +func TestUnsetArrayItem(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + TreeBranch{ + TreeItem{ + Key: "barbar", + Value: "barbar", + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", 1}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, + }, unset) +} + +func TestUnsetKeyInArrayItem(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + TreeItem{ + Key: "barbar", + Value: "barbar", + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", 0, "barbar"}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, + }, unset) +} + +func TestUnsetArrayLastItem(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", 0}) + assert.NoError(t, err) + assert.Equal(t, TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + }, + }, + }, unset) +} + +func TestUnsetKeyNotFound(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", "unknown-value"}) + assert.Equal(t, err.(*SopsKeyNotFound).Key, "unknown-value") + assert.ErrorContains(t, err, "unknown-value") + assert.Nil(t, unset, "Unset result was not nil upon %s", err) +} + +func TestUnsetKeyInArrayNotFound(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", 0, "unknown"}) + assert.Equal(t, err.(*SopsKeyNotFound).Key, "unknown") + assert.Nil(t, unset, "Unset result was not nil upon %s", err) +} + +func TestUnsetArrayItemOutOfBounds(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: []interface{}{ + TreeBranch{ + TreeItem{ + Key: "bar", + Value: "bar", + }, + }, + }, + }, + } + unset, err := branch.Unset([]interface{}{"foo", 99}) + assert.Equal(t, err.(*SopsKeyNotFound).Key, 99) + assert.Nil(t, unset, "Unset result was not nil upon %s", err) +} + +func TestUnsetKeyNotABranch(t *testing.T) { + branch := TreeBranch{ + TreeItem{ + Key: "foo", + Value: 99, + }, + } + unset, err := branch.Unset([]interface{}{"foo", "bar"}) + assert.ErrorContains(t, err, "Unsupported type") + assert.Nil(t, unset, "Unset result was not nil upon %s", err) +} + func TestEmitAsMap(t *testing.T) { expected := map[string]interface{}{ "foobar": "barfoo", @@ -1128,6 +1524,10 @@ func TestEmitAsMap(t *testing.T) { Key: "number", Value: 42, }, + TreeItem{ + Key: Comment{"comment"}, + Value: nil, + }, }, TreeBranch{ TreeItem{ @@ -1149,9 +1549,8 @@ func TestEmitAsMap(t *testing.T) { data, err := EmitAsMap(branches) - if assert.NoError(t, err) { - assert.Equal(t, expected, data) - } + assert.NoError(t, err) + assert.Equal(t, expected, data) } func TestSortKeyGroupIndices(t *testing.T) { diff --git a/stores/ini/store.go b/stores/ini/store.go index 350142d85..83605000e 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -24,7 +24,8 @@ func NewStore(c *config.INIStoreConfig) *Store { } func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { - iniFile := ini.Empty() + iniFile := ini.Empty(ini.LoadOptions{AllowNonUniqueSections: true}) + iniFile.DeleteSection(ini.DefaultSection) for _, branch := range branches { for _, item := range branch { if _, ok := item.Key.(sops.Comment); ok { @@ -95,7 +96,7 @@ func (store Store) iniFromTreeBranches(branches sops.TreeBranches) ([]byte, erro } func (store Store) treeBranchesFromIni(in []byte) (sops.TreeBranches, error) { - iniFile, err := ini.Load(in) + iniFile, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in) if err != nil { return nil, err } @@ -143,7 +144,7 @@ func (store Store) treeItemFromSection(section *ini.Section) (sops.TreeItem, err // LoadEncryptedFile loads encrypted INI file's bytes onto a sops.Tree runtime object func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { - iniFileOuter, err := ini.Load(in) + iniFileOuter, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in) if err != nil { return sops.Tree{}, err } diff --git a/stores/ini/store_test.go b/stores/ini/store_test.go index 3e833b54c..b1aff6cf9 100644 --- a/stores/ini/store_test.go +++ b/stores/ini/store_test.go @@ -3,8 +3,8 @@ package ini import ( "testing" - "github.com/stretchr/testify/assert" "github.com/getsops/sops/v3" + "github.com/stretchr/testify/assert" ) func TestDecodeIni(t *testing.T) { @@ -127,6 +127,55 @@ func TestEncodeIniWithEscaping(t *testing.T) { assert.Equal(t, expected, branches) } +func TestEncodeIniWithDuplicateSections(t *testing.T) { + branches := sops.TreeBranches{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "DEFAULT", + Value: interface{}(sops.TreeBranch(nil)), + }, + sops.TreeItem{ + Key: "foo", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: "bar", + }, + sops.TreeItem{ + Key: "baz", + Value: "3.0", + }, + sops.TreeItem{ + Key: "qux", + Value: "false", + }, + }, + }, + sops.TreeItem{ + Key: "foo", + Value: sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: "bar", + }, + sops.TreeItem{ + Key: "baz", + Value: "3.0", + }, + sops.TreeItem{ + Key: "qux", + Value: "false", + }, + }, + }, + }, + } + out, err := Store{}.iniFromTreeBranches(branches) + assert.Nil(t, err) + expected, _ := Store{}.treeBranchesFromIni(out) + assert.Equal(t, expected, branches) +} + func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) { data := []byte(`hello=2`) store := Store{} diff --git a/stores/json/store.go b/stores/json/store.go index e9df1b554..7b8bf3da5 100644 --- a/stores/json/store.go +++ b/stores/json/store.go @@ -186,18 +186,20 @@ func (store Store) encodeValue(v interface{}) ([]byte, error) { func (store Store) encodeArray(array []interface{}) ([]byte, error) { out := "[" - for i, item := range array { + empty := true + for _, item := range array { if _, ok := item.(sops.Comment); ok { continue } + if !empty { + out += "," + } v, err := store.encodeValue(item) if err != nil { return nil, err } out += string(v) - if i != len(array)-1 { - out += "," - } + empty = false } out += "]" return []byte(out), nil @@ -205,10 +207,14 @@ func (store Store) encodeArray(array []interface{}) ([]byte, error) { func (store Store) encodeTree(tree sops.TreeBranch) ([]byte, error) { out := "{" - for i, item := range tree { + empty := true + for _, item := range tree { if _, ok := item.Key.(sops.Comment); ok { continue } + if !empty { + out += "," + } v, err := store.encodeValue(item.Value) if err != nil { return nil, fmt.Errorf("Error encoding value %s: %s", v, err) @@ -218,9 +224,7 @@ func (store Store) encodeTree(tree sops.TreeBranch) ([]byte, error) { return nil, fmt.Errorf("Error encoding key %s: %s", k, err) } out += string(k) + `: ` + string(v) - if i != len(tree)-1 { - out += "," - } + empty = false } return []byte(out + "}"), nil } @@ -326,6 +330,7 @@ func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { if err != nil { return nil, fmt.Errorf("Error marshaling to json: %s", err) } + out = append(out, '\n') return out, nil } @@ -336,6 +341,7 @@ func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) { if err != nil { return nil, fmt.Errorf("Error marshaling to json: %s", err) } + out = append(out, '\n') return out, nil } diff --git a/stores/json/store_test.go b/stores/json/store_test.go index eca6d3e96..0f447d869 100644 --- a/stores/json/store_test.go +++ b/stores/json/store_test.go @@ -34,7 +34,8 @@ func TestDecodeJSON(t *testing.T) { } } } -}` +} +` expected := sops.TreeBranch{ sops.TreeItem{ Key: "glossary", @@ -312,7 +313,8 @@ func TestEncodeJSONArrayOfObjects(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: -1, @@ -446,7 +448,8 @@ func TestIndentTwoSpaces(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: 2, @@ -488,7 +491,8 @@ func TestIndentDefault(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: -1, @@ -530,7 +534,8 @@ func TestNoIndent(t *testing.T) { }, 2 ] -}` +} +` store := Store{ config: config.JSONStoreConfig{ Indent: 0, @@ -539,4 +544,94 @@ func TestNoIndent(t *testing.T) { out, err := store.EmitPlainFile(tree.Branches) assert.Nil(t, err) assert.Equal(t, expected, string(out)) + +} + +func TestConflictingAttributes(t *testing.T) { + // See https://stackoverflow.com/a/23195243 + // Duplicate keys in json is technically valid, but discouraged. + // Implementations may handle them differently. ECMA-262 says + // + // > In the case where there are duplicate name Strings within an object, + // > lexically preceding values for the same key shall be overwritten. + + data := ` +{ + "hello": "Sops config file", + "hello": "Doubles are ok", + "hello": ["repeatedly"], + "hello": 3.14 +} +` + s := new(Store) + _, err := s.LoadPlainFile([]byte(data)) + assert.Nil(t, err) +} + +func TestComments(t *testing.T) { + tree := sops.Tree{ + Branches: sops.TreeBranches{ + sops.TreeBranch{ + sops.TreeItem{ + Key: "foo", + Value: []interface{}{ + sops.Comment{Value: " comment 0"}, + sops.TreeBranch{ + sops.TreeItem{ + Key: sops.Comment{Value: " comment 1"}, + Value: nil, + }, + sops.TreeItem{ + Key: "foo", + Value: 3, + }, + sops.TreeItem{ + Key: sops.Comment{Value: " comment 2"}, + Value: nil, + }, + sops.TreeItem{ + Key: sops.Comment{Value: " comment 3"}, + Value: nil, + }, + sops.TreeItem{ + Key: "bar", + Value: false, + }, + sops.TreeItem{ + Key: sops.Comment{Value: " comment 4"}, + Value: nil, + }, + sops.TreeItem{ + Key: sops.Comment{Value: " comment 5"}, + Value: nil, + }, + }, + sops.Comment{Value: " comment 6"}, + sops.Comment{Value: " comment 7"}, + 2, + sops.Comment{Value: " comment 8"}, + sops.Comment{Value: " comment 9"}, + }, + }, + }, + }, + } + expected := `{ + "foo": [ + { + "foo": 3, + "bar": false + }, + 2 + ] +} +` + store := Store{ + config: config.JSONStoreConfig{ + Indent: 2, + }, + } + out, err := store.EmitPlainFile(tree.Branches) + assert.Nil(t, err) + assert.Equal(t, expected, string(out)) } diff --git a/stores/yaml/store.go b/stores/yaml/store.go index 697ff10e9..5d5c41d8d 100644 --- a/stores/yaml/store.go +++ b/stores/yaml/store.go @@ -307,6 +307,13 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { // sops.Tree runtime object func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { var branches sops.TreeBranches + if len(in) > 0 { + // This is needed to make the yaml-decoder check for uniqueness of keys + // Can probably be removed when https://github.com/go-yaml/yaml/issues/814 is merged. + if err := yaml.NewDecoder(bytes.NewReader(in)).Decode(make(map[string]interface{})); err != nil { + return nil, err + } + } d := yaml.NewDecoder(bytes.NewReader(in)) for { var data yaml.Node @@ -322,6 +329,12 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { if err != nil { return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err) } + // Prevent use of reserved keywords + for _, item := range branch { + if item.Key == stores.SopsMetadataKey { + return nil, fmt.Errorf("YAML doc used reserved word '%v'", item.Key) + } + } branches = append(branches, branch) } return branches, nil diff --git a/stores/yaml/store_test.go b/stores/yaml/store_test.go index fb53d9ed3..5c8a59953 100644 --- a/stores/yaml/store_test.go +++ b/stores/yaml/store_test.go @@ -62,19 +62,19 @@ key4: *bar var ALIASES_BRANCHES = sops.TreeBranches{ sops.TreeBranch{ sops.TreeItem{ - Key: "key1", + Key: "key1", Value: []interface{}{ "foo", }, }, sops.TreeItem{ - Key: "key2", + Key: "key2", Value: []interface{}{ "foo", }, }, sops.TreeItem{ - Key: "key3", + Key: "key3", Value: sops.TreeBranch{ sops.TreeItem{ Key: "foo", @@ -87,7 +87,7 @@ var ALIASES_BRANCHES = sops.TreeBranches{ }, }, sops.TreeItem{ - Key: "key4", + Key: "key4", Value: sops.TreeBranch{ sops.TreeItem{ Key: "foo", @@ -237,7 +237,6 @@ prometheus-node-exporter: - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$ `) - func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) { data := []byte(`hello: 2`) _, err := (&Store{}).LoadEncryptedFile(data) @@ -398,3 +397,35 @@ func TestHasSopsTopLevelKey(t *testing.T) { }) assert.Equal(t, ok, false) } + +func TestDuplicateAttributes(t *testing.T) { + // Duplicate keys are _not_ valid yaml. + // + // See https://yaml.org/spec/1.2.2/#mapping + // > The content of a mapping node is an unordered set of key/value node pairs, + // > with the restriction that each of the keys is unique. + // + data := ` +hello: Sops config file +hello: Duplicates are not ok +rootunique: + key2: "value" + key2: "foo" +` + s := new(Store) + _, err := s.LoadPlainFile([]byte(data)) + assert.NotNil(t, err) + assert.Equal(t, `yaml: unmarshal errors: + line 3: mapping key "hello" already defined at line 2`, err.Error()) +} + +func TestReservedAttributes(t *testing.T) { + data := ` +hello: Sops config file +sops: The attribute 'sops' must be rejected, otherwise the file cannot be opened later on +` + s := new(Store) + _, err := s.LoadPlainFile([]byte(data)) + assert.NotNil(t, err) + assert.Equal(t, `YAML doc used reserved word 'sops'`, err.Error()) +} diff --git a/version/version.go b/version/version.go index a40cc8ec8..01c2640c1 100644 --- a/version/version.go +++ b/version/version.go @@ -12,11 +12,13 @@ import ( ) // Version represents the value of the current semantic version. -var Version = "3.9.0" +var Version = "3.9.4" // PrintVersion prints the current version of sops. If the flag -// `--disable-version-check` is set, the function will not attempt -// to retrieve the latest version from the GitHub API. +// `--disable-version-check` is set or if the environment variable +// SOPS_DISABLE_VERSION_CHECK is set to a value that is considered +// true by https://pkg.go.dev/strconv#ParseBool, the function will +// not attempt to retrieve the latest version from the GitHub API. // // If the flag is not set, the function will attempt to retrieve // the latest version from the GitHub API and compare it to the