pax_global_header00006660000000000000000000000064147417301430014516gustar00rootroot0000000000000052 comment=995f5df1f5c175c8be97a7f66e0ca136fcd5b83f golang-github-spiffe-go-spiffe-2.4.0/000077500000000000000000000000001474173014300173775ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/.gitattributes000066400000000000000000000000411474173014300222650ustar00rootroot00000000000000# Set autocrlf = false * -text golang-github-spiffe-go-spiffe-2.4.0/.github/000077500000000000000000000000001474173014300207375ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/.github/dependabot.yml000066400000000000000000000003051474173014300235650ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly - package-ecosystem: gomod directory: /v2 schedule: interval: weekly golang-github-spiffe-go-spiffe-2.4.0/.github/workflows/000077500000000000000000000000001474173014300227745ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/.github/workflows/pr_build.yaml000066400000000000000000000045371474173014300254710ustar00rootroot00000000000000name: PR Build on: pull_request: {} workflow_dispatch: {} env: GO_VERSION: 1.21 jobs: lint-linux: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v5 with: cache: true cache-dependency-path: v2/go.sum go-version: ${{ env.GO_VERSION }} - name: Lint run: make lint test-linux: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v5 with: cache: true cache-dependency-path: v2/go.sum go-version: ${{ env.GO_VERSION }} - name: Test run: make test lint-windows: runs-on: windows-2022 defaults: run: shell: msys2 {0} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v5 with: cache: true cache-dependency-path: v2/go.sum go-version: ${{ env.GO_VERSION }} - name: Install msys2 uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: >- git base-devel mingw-w64-x86_64-toolchain unzip - name: Lint run: make lint test-windows: runs-on: windows-2022 defaults: run: shell: msys2 {0} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup go uses: actions/setup-go@v5 with: cache: true cache-dependency-path: v2/go.sum go-version: ${{ env.GO_VERSION }} - name: Install msys2 uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: >- git base-devel mingw-w64-x86_64-toolchain unzip - name: Test run: make test # This job is just here to make sure that the other jobs have completed # and is used as a single job to block PR merge from. GH doesn't have a # way to say "all jobs from this action", which would be ideal. success: needs: [lint-linux, test-linux, lint-windows, test-windows] runs-on: ubuntu-latest steps: - name: Shout it out run: echo SUCCESS golang-github-spiffe-go-spiffe-2.4.0/.gitignore000066400000000000000000000000671474173014300213720ustar00rootroot00000000000000.build/ # Editor specific configuration .idea .vscode golang-github-spiffe-go-spiffe-2.4.0/CHANGELOG.md000066400000000000000000000044551474173014300212200ustar00rootroot00000000000000# Changelog ## [2.4.0] - 2024-10-05 ### Added - Support for using a custom backoff strategy in the Workload API client (#302) - Support for a default JWT-SVID picker (#301) ## [2.3.0] - 2024-06-17 ### Changed - Empty bundles are now supported, in alignment with the SPIFFE specification (#288) ## [2.2.0] - 2024-04-01 ### Changed - Upgraded to go-jose v4 which has a stronger security posture than v3. Go-spiffe was not impacted by the security weaknesses of v3 due to stringing algorithm checking that is now handled by go-jose v4 (#276) ### Fixed - Makefile invocation for Apple Silicon-based Macs (#275) ### Added - Support Ed25519 keys for Workload SVIDs (#248) ## [2.1.7] - 2024-01-17 ### Fixed - Panic if the Workload API returned a malformed JWT-SVID (#233) - Race that causes WaitForUpdate to return immediately after watcher is initialized even if there is no update (#260) ## [2.1.6] - 2023-06-06 ### Added - Name convenience method to the spiffeid.TrustDomain type (#228) ## [2.1.5] - 2023-05-26 ### Added - PeerIDFromConnectionState method for extracting the peer ID from TLS connection state (#225) ### Changed - The `tlsconfig` to enforce a minimum TLS version of TLS1.2 (#226) ### Fixed - Panic when failing to parse raw SVID response returned from the Workload API (#223) ## [2.1.4] - 2023-03-31 ### Added - Support for the SVID hints obtained from the Workload API (#220) ## [2.1.3] - 2023-03-16 ### Changed - JoinPathSegments properly disallows dot segments (#221) ### Added - ValidatePathSegment function for validating an individual path segment (#221) ## [2.1.2] - 2023-01-09 ### Changed - Minimum supported go version to 1.17 (#209) ## [2.1.1] - 2022-06-29 ### Added - Support for dialing named pipes using an npipe URL scheme (#198) ## [2.1.0] - 2022-04-29 ### Added - The workloadapi.WatchX509Bundles method which watches X.509 bundles from the Workload API (#192) - The workloadapi.WithNamedPipeName option to support connecting to the Workload API via named pipes (#190) - The workloadapi.FetchJWTSVIDs method which fetches multiple JWT-SVIDs from the Workload API, instead of just the first (#187) - The x509bundle.ParseRaw method for creating a bundle from raw ASN.1 encoded certificates (#192) ### Changed - The spiffeid.ID String() method no longer causes an allocation (#185) golang-github-spiffe-go-spiffe-2.4.0/CODEOWNERS000066400000000000000000000000451474173014300207710ustar00rootroot00000000000000* @amartinezfayo @evan2645 @azdagron golang-github-spiffe-go-spiffe-2.4.0/LICENSE000066400000000000000000000261351474173014300204130ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-spiffe-go-spiffe-2.4.0/Makefile000066400000000000000000000153451474173014300210470ustar00rootroot00000000000000.PHONY: default all help default: all all: lint test help: @echo "Usage: make " @echo @echo " all - lints and tests the code" @echo " lint - lints the code" @echo " test - tests the code" @echo " generate - generate protocol buffer and gRPC stub code (default)" @echo " generate-check - ensure generated code is up to date" # Used to force some rules to run every time FORCE: ; ############################################################################ # OS/ARCH detection ############################################################################ os1=$(shell uname -s) os2= ifeq ($(os1),Darwin) os1=darwin os2=osx else ifeq ($(os1),Linux) os1=linux os2=linux else ifeq (,$(findstring MYSYS_NT-10-0-, $(os1))) os1=windows os2=windows else $(error unsupported OS: $(os1)) endif arch1=$(shell uname -m) ifeq ($(arch1),x86_64) arch2=amd64 else ifeq ($(arch1),aarch64) arch2=arm64 else ifeq ($(arch1),arm64) arch2=arm64 else $(error unsupported ARCH: $(arch1)) endif ############################################################################# # Vars ############################################################################# build_dir := ${CURDIR}/.build/$(os1)-$(arch1) protoc_version = 3.14.0 ifeq ($(os1),windows) protoc_url = https://github.com/protocolbuffers/protobuf/releases/download/v$(protoc_version)/protoc-$(protoc_version)-win64.zip else ifeq ($(arch1),aarch64) protoc_url = https://github.com/protocolbuffers/protobuf/releases/download/v$(protoc_version)/protoc-$(protoc_version)-$(os2)-aarch_64.zip else protoc_url = https://github.com/protocolbuffers/protobuf/releases/download/v$(protoc_version)/protoc-$(protoc_version)-$(os2)-$(arch1).zip endif protoc_dir = $(build_dir)/protoc/$(protoc_version) protoc_bin = $(protoc_dir)/bin/protoc protoc_gen_go_version := $(shell grep google.golang.org/protobuf v2/go.mod | awk '{print $$2}') protoc_gen_go_base_dir := $(build_dir)/protoc-gen-go protoc_gen_go_dir := $(protoc_gen_go_base_dir)/$(protoc_gen_go_version)-go$(go_version) protoc_gen_go_bin := $(protoc_gen_go_dir)/protoc-gen-go protoc_gen_go_grpc_version := v1.0.1 protoc_gen_go_grpc_base_dir := $(build_dir)/protoc-gen-go-grpc protoc_gen_go_grpc_dir := $(protoc_gen_go_grpc_base_dir)/$(protoc_gen_go_grpc_version)-go$(go_version) protoc_gen_go_grpc_bin := $(protoc_gen_go_grpc_dir)/protoc-gen-go-grpc golangci_lint_version = v1.57.2 golangci_lint_dir = $(build_dir)/golangci_lint/$(golangci_lint_version) golangci_lint_bin = $(golangci_lint_dir)/golangci-lint apiprotos := \ v2/proto/spiffe/workload/workload.proto \ ############################################################################# # Toolchain ############################################################################# go_version_full := 1.21.8 go_version := $(go_version_full:.0=) go_dir := $(build_dir)/go/$(go_version) ifeq ($(os1),windows) go_bin_dir = $(go_dir)/go/bin go_url = https://storage.googleapis.com/golang/go$(go_version).$(os1)-$(arch2).zip exe=".exe" else go_bin_dir = $(go_dir)/bin go_url = https://storage.googleapis.com/golang/go$(go_version).$(os1)-$(arch2).tar.gz exe= endif go_path := PATH="$(go_bin_dir):$(PATH)" go-check: ifeq (go$(go_version), $(shell $(go_path) go version 2>/dev/null | cut -f3 -d' ')) else ifeq ($(os1),windows) @echo "Installing go$(go_version)..." rm -rf $(dir $(go_dir)) mkdir -p $(go_dir) curl -o $(go_dir)\go.zip -sSfL $(go_url) unzip -qq $(go_dir)\go.zip -d $(go_dir) else @echo "Installing go$(go_version)..." $(E)rm -rf $(dir $(go_dir)) $(E)mkdir -p $(go_dir) $(E)curl -sSfL $(go_url) | tar xz -C $(go_dir) --strip-components=1 endif ############################################################################# # Linting ############################################################################# .PHONY: lint lint: $(golangci_lint_bin) | go-check @cd ./v2; PATH="$(go_bin_dir):$(PATH)" $(golangci_lint_bin) run ./... $(golangci_lint_bin): @echo "Installing golangci-lint $(golangci_lint_version)..." @rm -rf $(dir $(golangci_lint_dir)) @mkdir -p $(golangci_lint_dir) @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(golangci_lint_dir) $(golangci_lint_version) ############################################################################# # Tidy ############################################################################# .PHONY: test tidy: | go-check @cd ./v2; $(go_path) go mod tidy ############################################################################# # Testing ############################################################################# .PHONY: test test: | go-check @cd ./v2; $(go_path) go test -race ./... ############################################################################# # Code Generation ############################################################################# .PHONY: generate generate: $(apiprotos:.proto=.pb.go) $(apiprotos:.proto=_grpc.pb.go) %_grpc.pb.go: %.proto $(protoc_bin) $(protoc_gen_go_grpc_bin) FORCE @echo "compiling proto $<..." @cd "$(dir $<)" && PATH="$(protoc_gen_go_grpc_dir):$(PATH)" \ $(protoc_bin) \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ $(notdir $<) %.pb.go: %.proto $(protoc_bin) $(protoc_gen_go_bin) FORCE @echo "compiling gRPC $<..." @cd "$(dir $<)" && PATH="$(protoc_gen_go_dir):$(PATH)" \ $(protoc_bin) \ --go_out=. --go_opt=paths=source_relative \ $(notdir $<) $(protoc_bin): @echo "Installing protoc $(protoc_version)..." @rm -rf $(dir $(protoc_dir)) @mkdir -p $(protoc_dir) @curl -sSfL $(protoc_url) -o $(build_dir)/tmp.zip; unzip -q -d $(protoc_dir) $(build_dir)/tmp.zip; rm $(build_dir)/tmp.zip $(protoc_gen_go_bin): | go-check @echo "Installing protoc-gen-go $(protoc_gen_go_version)..." @rm -rf $(protoc_gen_go_base_dir) @mkdir -p $(protoc_gen_go_dir) @GOBIN="$(protoc_gen_go_dir)" $(go_path) go install google.golang.org/protobuf/cmd/protoc-gen-go@$(protoc_gen_go_version) $(protoc_gen_go_grpc_bin): | go-check @echo "Installing protoc-gen-go-grpc $(protoc_gen_go_grpc_version)..." @rm -rf $(protoc_gen_go_grpc_base_dir) @mkdir -p $(protoc_gen_go_grpc_dir) @GOBIN=$(protoc_gen_go_grpc_dir) $(go_path) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(protoc_gen_go_grpc_version) ############################################################################# # Code Generation Checks ############################################################################# git_dirty := $(shell git status -s) .PHONY: generate-check generate-check: ifneq ($(git_dirty),) $(error generate-check must be invoked on a clean repository) endif @$(MAKE) generate @$(MAKE) git-clean-check .PHONY: git-clean-check git-clean-check: ifneq ($(git_dirty),) git diff @echo "Git repository is dirty!" @false else @echo "Git repository is clean." endif golang-github-spiffe-go-spiffe-2.4.0/README.md000066400000000000000000000036631474173014300206660ustar00rootroot00000000000000# go-spiffe (v2) This library is a convenient Go library for working with [SPIFFE](https://spiffe.io/). It leverages the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md), providing high level functionality that includes: * Establishing mutually authenticated TLS (__mTLS__) between workloads powered by SPIFFE. * Obtaining and validating [X509-SVIDs](https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md) and [JWT-SVIDs](https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md). * Federating trust between trust domains using [SPIFFE bundles](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#3-spiffe-bundles). * Bundle management. ## Documentation See the [Go Package](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2) documentation. ## Quick Start Prerequisites: 1. Running [SPIRE](https://spiffe.io/spire/) or another SPIFFE Workload API implementation. 2. `SPIFFE_ENDPOINT_SOCKET` environment variable set to address of the Workload API (e.g. `unix:///tmp/agent.sock`). Alternatively the socket address can be provided programatically. To create an mTLS server: ```go listener, err := spiffetls.Listen(ctx, "tcp", "127.0.0.1:8443", tlsconfig.AuthorizeAny()) ``` To dial an mTLS server: ```go conn, err := spiffetls.Dial(ctx, "tcp", "127.0.0.1:8443", tlsconfig.AuthorizeAny()) ``` The client and server obtain [X509-SVIDs](https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md) and X.509 bundles from the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md). The X509-SVIDs are presented by each peer and authenticated against the X.509 bundles. Both sides continue to be updated with X509-SVIDs and X.509 bundles streamed from the Workload API (e.g. secret rotation). ## Examples The [examples](./v2/examples) directory contains rich examples for a variety of circumstances. golang-github-spiffe-go-spiffe-2.4.0/v2/000077500000000000000000000000001474173014300177265ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/.golangci.yml000066400000000000000000000015101474173014300223070ustar00rootroot00000000000000run: # timeout for analysis, e.g. 30s, 5m, default is 1m deadline: 10m linters: enable: - bodyclose - goimports - revive - gosec - misspell - nakedret - exportloopref - unconvert - unparam - whitespace - gocritic issues: # include examples exclude-dirs-use-default: false exclude-dirs: - testdata$ - test/mock exclude-files: - ".*\\.pb\\.go" exclude-rules: # exclude some lints from examples test files - path: examples_test.go linters: - staticcheck - ineffassign - govet linters-settings: golint: # minimal confidence for issues, default is 0.8 min-confidence: 0.0 revive: rules: - name: unused-parameter disabled: true # It's useful to name parameters in library code for better readability golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/000077500000000000000000000000001474173014300211775ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/000077500000000000000000000000001474173014300231755ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/bundle.go000066400000000000000000000130431474173014300247760ustar00rootroot00000000000000package jwtbundle import ( "crypto" "encoding/json" "io" "os" "sync" "github.com/go-jose/go-jose/v4" "github.com/spiffe/go-spiffe/v2/internal/jwtutil" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) var jwtbundleErr = errs.Class("jwtbundle") // Bundle is a collection of trusted JWT authorities for a trust domain. type Bundle struct { trustDomain spiffeid.TrustDomain mtx sync.RWMutex jwtAuthorities map[string]crypto.PublicKey } // New creates a new bundle. func New(trustDomain spiffeid.TrustDomain) *Bundle { return &Bundle{ trustDomain: trustDomain, jwtAuthorities: make(map[string]crypto.PublicKey), } } // FromJWTAuthorities creates a new bundle from JWT authorities func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle { return &Bundle{ trustDomain: trustDomain, jwtAuthorities: jwtutil.CopyJWTAuthorities(jwtAuthorities), } } // Load loads a bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document. func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) { bundleBytes, err := os.ReadFile(path) if err != nil { return nil, jwtbundleErr.New("unable to read JWT bundle: %w", err) } return Parse(trustDomain, bundleBytes) } // Read decodes a bundle from a reader. The contents must contain a standard RFC 7517 JWKS document. func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) { b, err := io.ReadAll(r) if err != nil { return nil, jwtbundleErr.New("unable to read: %v", err) } return Parse(trustDomain, b) } // Parse parses a bundle from bytes. The data must be a standard RFC 7517 JWKS document. func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) { jwks := new(jose.JSONWebKeySet) if err := json.Unmarshal(bundleBytes, jwks); err != nil { return nil, jwtbundleErr.New("unable to parse JWKS: %v", err) } bundle := New(trustDomain) for i, key := range jwks.Keys { if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil { return nil, jwtbundleErr.New("error adding authority %d of JWKS: %v", i, errs.Unwrap(err)) } } return bundle, nil } // TrustDomain returns the trust domain that the bundle belongs to. func (b *Bundle) TrustDomain() spiffeid.TrustDomain { return b.trustDomain } // JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID. func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey { b.mtx.RLock() defer b.mtx.RUnlock() return jwtutil.CopyJWTAuthorities(b.jwtAuthorities) } // FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority // is found, it is returned and the boolean is true. Otherwise, the returned // value is nil and the boolean is false. func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) { b.mtx.RLock() defer b.mtx.RUnlock() if jwtAuthority, ok := b.jwtAuthorities[keyID]; ok { return jwtAuthority, true } return nil, false } // HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID. func (b *Bundle) HasJWTAuthority(keyID string) bool { b.mtx.RLock() defer b.mtx.RUnlock() _, ok := b.jwtAuthorities[keyID] return ok } // AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists // under the given key ID, it is replaced. A key ID must be specified. func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error { if keyID == "" { return jwtbundleErr.New("keyID cannot be empty") } b.mtx.Lock() defer b.mtx.Unlock() b.jwtAuthorities[keyID] = jwtAuthority return nil } // RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle. func (b *Bundle) RemoveJWTAuthority(keyID string) { b.mtx.Lock() defer b.mtx.Unlock() delete(b.jwtAuthorities, keyID) } // SetJWTAuthorities sets the JWT authorities in the bundle. func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) { b.mtx.Lock() defer b.mtx.Unlock() b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities) } // Empty returns true if the bundle has no JWT authorities. func (b *Bundle) Empty() bool { b.mtx.RLock() defer b.mtx.RUnlock() return len(b.jwtAuthorities) == 0 } // Marshal marshals the JWT bundle into a standard RFC 7517 JWKS document. The // JWKS does not contain any SPIFFE-specific parameters. func (b *Bundle) Marshal() ([]byte, error) { b.mtx.RLock() defer b.mtx.RUnlock() jwks := jose.JSONWebKeySet{} for keyID, jwtAuthority := range b.jwtAuthorities { jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ Key: jwtAuthority, KeyID: keyID, }) } return json.Marshal(jwks) } // Clone clones the bundle. func (b *Bundle) Clone() *Bundle { b.mtx.RLock() defer b.mtx.RUnlock() return FromJWTAuthorities(b.trustDomain, b.jwtAuthorities) } // Equal compares the bundle for equality against the given bundle. func (b *Bundle) Equal(other *Bundle) bool { if b == nil || other == nil { return b == other } return b.trustDomain == other.trustDomain && jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) } // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. It implements the Source interface. An error will be returned if // the trust domain does not match that of the bundle. func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { b.mtx.RLock() defer b.mtx.RUnlock() if b.trustDomain != trustDomain { return nil, jwtbundleErr.New("no JWT bundle for trust domain %q", trustDomain) } return b, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/bundle_test.go000066400000000000000000000173501474173014300260420ustar00rootroot00000000000000package jwtbundle_test import ( "crypto" "os" "testing" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/errstrings" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testFile struct { filePath string keysCount int } var ( td = spiffeid.RequireTrustDomainFromString("example.org") testFiles = map[string]testFile{ "valid 1": { filePath: "testdata/jwks_valid_1.json", keysCount: 1, }, "valid 2": { filePath: "testdata/jwks_valid_2.json", keysCount: 2, }, "non existent file": { filePath: "testdata/does-not-exist.json", }, "missing kid": { filePath: "testdata/jwks_missing_kid.json", }, } ) func TestNew(t *testing.T) { b := jwtbundle.New(td) require.NotNil(t, b) require.Len(t, b.JWTAuthorities(), 0) require.Equal(t, td, b.TrustDomain()) } func TestFromJWTAuthorities(t *testing.T) { jwtAuthorities := map[string]crypto.PublicKey{ "key-1": "test-1", "key-2": "test-2", } b := jwtbundle.FromJWTAuthorities(td, jwtAuthorities) require.NotNil(t, b) assert.Equal(t, b.JWTAuthorities(), jwtAuthorities) } func TestLoad(t *testing.T) { testCases := []struct { tf testFile err string }{ { tf: testFiles["valid 1"], }, { tf: testFiles["valid 2"], }, { tf: testFiles["non existent file"], err: "jwtbundle: unable to read JWT bundle: open testdata/does-not-exist.json: " + errstrings.FileNotFound, }, { tf: testFiles["missing kid"], err: "jwtbundle: error adding authority 1 of JWKS: keyID cannot be empty", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.tf.filePath, func(t *testing.T) { bundle, err := jwtbundle.Load(td, testCase.tf.filePath) if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) require.NotNil(t, bundle) assert.Len(t, bundle.JWTAuthorities(), testCase.tf.keysCount) }) } } func TestRead(t *testing.T) { testCases := []struct { tf testFile err string }{ { tf: testFiles["valid 1"], }, { tf: testFiles["valid 2"], }, { tf: testFiles["non existent file"], err: "jwtbundle: unable to read: invalid argument", }, { tf: testFiles["missing kid"], err: "jwtbundle: error adding authority 1 of JWKS: keyID cannot be empty", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.tf.filePath, func(t *testing.T) { // we expect the Open call to fail in some cases file, _ := os.Open(testCase.tf.filePath) defer file.Close() bundle, err := jwtbundle.Read(td, file) if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) require.NotNil(t, bundle) assert.Len(t, bundle.JWTAuthorities(), testCase.tf.keysCount) }) } } func TestParse(t *testing.T) { testCases := []struct { tf testFile err string }{ { tf: testFiles["valid 1"], }, { tf: testFiles["valid 2"], }, { tf: testFiles["non existent file"], err: "jwtbundle: unable to parse JWKS: unexpected end of JSON input", }, { tf: testFiles["missing kid"], err: "jwtbundle: error adding authority 1 of JWKS: keyID cannot be empty", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.tf.filePath, func(t *testing.T) { // we expect the ReadFile call to fail in some cases bundleBytes, _ := os.ReadFile(testCase.tf.filePath) bundle, err := jwtbundle.Parse(td, bundleBytes) if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) require.NotNil(t, bundle) assert.Len(t, bundle.JWTAuthorities(), testCase.tf.keysCount) }) } } func TestTrustDomain(t *testing.T) { b := jwtbundle.New(td) btd := b.TrustDomain() require.Equal(t, td, btd) } func TestJWTAuthoritiesCRUD(t *testing.T) { // Test AddJWTAuthority (missing authority) b := jwtbundle.New(td) err := b.AddJWTAuthority("", "test-1") require.EqualError(t, err, "jwtbundle: keyID cannot be empty") // Test AddJWTAuthority (new authority) err = b.AddJWTAuthority("key-1", "test-1") require.NoError(t, err) // Test JWTAuthorities jwtAuthorities := b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{"key-1": "test-1"}, jwtAuthorities) err = b.AddJWTAuthority("key-2", "test-2") require.NoError(t, err) jwtAuthorities = b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{ "key-1": "test-1", "key-2": "test-2", }, jwtAuthorities) // Test FindJWTAuthority authority, ok := b.FindJWTAuthority("key-1") require.True(t, ok) require.Equal(t, "test-1", authority) authority, ok = b.FindJWTAuthority("key-3") require.False(t, ok) require.Nil(t, authority) require.Equal(t, true, b.HasJWTAuthority("key-1")) b.RemoveJWTAuthority("key-3") require.Equal(t, 2, len(b.JWTAuthorities())) require.Equal(t, true, b.HasJWTAuthority("key-1")) require.Equal(t, true, b.HasJWTAuthority("key-2")) // Test RemoveJWTAuthority b.RemoveJWTAuthority("key-2") require.Equal(t, 1, len(b.JWTAuthorities())) require.Equal(t, true, b.HasJWTAuthority("key-1")) // Test AddJWTAuthority (update authority) err = b.AddJWTAuthority("key-1", "test-1-updated") require.NoError(t, err) jwtAuthorities = b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{ "key-1": "test-1-updated", }, jwtAuthorities) } func TestMarshal(t *testing.T) { // Load a bundle to marshal bundle, err := jwtbundle.Load(td, "testdata/jwks_valid_2.json") require.NoError(t, err) // Marshal the bundle bundleBytesMarshal, err := bundle.Marshal() require.NoError(t, err) require.NotNil(t, bundleBytesMarshal) // Parse the marshaled bundle bundleParsed, err := jwtbundle.Parse(td, bundleBytesMarshal) require.NoError(t, err) // Assert that the marshaled bundle is equal to the parsed bundle assert.Equal(t, bundleParsed, bundle) } func TestGetJWTBundleForTrustDomain(t *testing.T) { b := jwtbundle.New(td) b1, err := b.GetJWTBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, b, b1) td2 := spiffeid.RequireTrustDomainFromString("example-2.org") b2, err := b.GetJWTBundleForTrustDomain(td2) require.Nil(t, b2) require.EqualError(t, err, `jwtbundle: no JWT bundle for trust domain "example-2.org"`) } func TestEqual(t *testing.T) { ca1 := test.NewCA(t, td) ca2 := test.NewCA(t, td2) empty := jwtbundle.New(td) empty2 := jwtbundle.New(td2) jwtAuthorities1 := jwtbundle.FromJWTAuthorities(td, ca1.JWTAuthorities()) jwtAuthorities2 := jwtbundle.FromJWTAuthorities(td, ca2.JWTAuthorities()) for _, tt := range []struct { name string a *jwtbundle.Bundle b *jwtbundle.Bundle expectEqual bool }{ { name: "empty equal", a: empty, b: empty, expectEqual: true, }, { name: "different trust domains", a: empty, b: empty2, expectEqual: false, }, { name: "JWT authorities equal", a: jwtAuthorities1, b: jwtAuthorities1, expectEqual: true, }, { name: "JWT authorities empty and not empty", a: empty, b: jwtAuthorities1, expectEqual: false, }, { name: "JWT authorities not empty but not equal", a: jwtAuthorities1, b: jwtAuthorities2, expectEqual: false, }, } { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.expectEqual, tt.a.Equal(tt.b)) }) } } func TestClone(t *testing.T) { // Load a bundle to clone original, err := jwtbundle.Load(td, "testdata/jwks_valid_2.json") require.NoError(t, err) cloned := original.Clone() require.True(t, original.Equal(cloned)) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/doc.go000066400000000000000000000025541474173014300242770ustar00rootroot00000000000000// Package jwtbundle provides JWT bundle related functionality. // // A bundle represents a collection of JWT authorities, i.e., those that // are used to authenticate SPIFFE JWT-SVIDs. // // You can create a new bundle for a specific trust domain: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := jwtbundle.New(td) // // Or you can load it from disk: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := jwtbundle.Load(td, "bundle.jwks") // // The bundle can be initialized with JWT authorities: // // td := spiffeid.RequireTrustDomain("example.org") // var jwtAuthorities map[string]crypto.PublicKey = ... // bundle := jwtbundle.FromJWTAuthorities(td, jwtAuthorities) // // In addition, you can add JWT authorities to the bundle: // // var keyID string = ... // var publicKey crypto.PublicKey = ... // bundle.AddJWTAuthority(keyID, publicKey) // // Bundles can be organized into a set, keyed by trust domain: // // set := jwtbundle.NewSet() // set.Add(bundle) // // A Source is source of JWT bundles for a trust domain. Both the Bundle // and Set types implement Source: // // // Initialize the source from a bundle or set // var source jwtbundle.Source = bundle // // ... or ... // var source jwtbundle.Source = set // // // Use the source to query for bundles by trust domain // bundle, err := source.GetJWTBundleForTrustDomain(td) package jwtbundle golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/set.go000066400000000000000000000046311474173014300243230ustar00rootroot00000000000000package jwtbundle import ( "sort" "sync" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Set is a set of bundles, keyed by trust domain. type Set struct { mtx sync.RWMutex bundles map[spiffeid.TrustDomain]*Bundle } // NewSet creates a new set initialized with the given bundles. func NewSet(bundles ...*Bundle) *Set { bundlesMap := make(map[spiffeid.TrustDomain]*Bundle) for _, b := range bundles { if b != nil { bundlesMap[b.trustDomain] = b } } return &Set{ bundles: bundlesMap, } } // Add adds a new bundle into the set. If a bundle already exists for the // trust domain, the existing bundle is replaced. func (s *Set) Add(bundle *Bundle) { s.mtx.Lock() defer s.mtx.Unlock() if bundle != nil { s.bundles[bundle.trustDomain] = bundle } } // Remove removes the bundle for the given trust domain. func (s *Set) Remove(trustDomain spiffeid.TrustDomain) { s.mtx.Lock() defer s.mtx.Unlock() delete(s.bundles, trustDomain) } // Has returns true if there is a bundle for the given trust domain. func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool { s.mtx.RLock() defer s.mtx.RUnlock() _, ok := s.bundles[trustDomain] return ok } // Get returns a bundle for the given trust domain. If the bundle is in the set // it is returned and the boolean is true. Otherwise, the returned value is // nil and the boolean is false. func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] return bundle, ok } // Bundles returns the bundles in the set sorted by trust domain. func (s *Set) Bundles() []*Bundle { s.mtx.RLock() defer s.mtx.RUnlock() out := make([]*Bundle, 0, len(s.bundles)) for _, bundle := range s.bundles { out = append(out, bundle) } sort.Slice(out, func(a, b int) bool { return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0 }) return out } // Len returns the number of bundles in the set. func (s *Set) Len() int { s.mtx.RLock() defer s.mtx.RUnlock() return len(s.bundles) } // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. It implements the Source interface. func (s *Set) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] if !ok { return nil, jwtbundleErr.New("no JWT bundle for trust domain %q", trustDomain) } return bundle, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/set_test.go000066400000000000000000000023201474173014300253530ustar00rootroot00000000000000package jwtbundle_test import ( "testing" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/require" ) var ( b1 = jwtbundle.New(td) td2 = spiffeid.RequireTrustDomainFromString("example-2.org") ) func TestNewSet(t *testing.T) { s := jwtbundle.NewSet(b1) require.True(t, s.Has(td)) s = jwtbundle.NewSet(jwtbundle.New(td), jwtbundle.New(td2)) require.True(t, s.Has(td)) require.True(t, s.Has(td2)) } func TestAdd(t *testing.T) { s := jwtbundle.NewSet() require.False(t, s.Has(td)) s.Add(b1) require.True(t, s.Has(td)) } func TestRemove(t *testing.T) { s := jwtbundle.NewSet(b1) require.True(t, s.Has(td)) s.Remove(td2) require.True(t, s.Has(td)) s.Remove(td) require.False(t, s.Has(td)) } func TestHas(t *testing.T) { s := jwtbundle.NewSet(jwtbundle.New(td)) require.False(t, s.Has(td2)) require.True(t, s.Has(td)) } func TestSetGetJWTBundleForTrustDomain(t *testing.T) { s := jwtbundle.NewSet(b1) _, err := s.GetJWTBundleForTrustDomain(td2) require.EqualError(t, err, `jwtbundle: no JWT bundle for trust domain "example-2.org"`) b, err := s.GetJWTBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, b1, b) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/source.go000066400000000000000000000005151474173014300250250ustar00rootroot00000000000000package jwtbundle import ( "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Source represents a source of JWT bundles keyed by trust domain. type Source interface { // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/testdata/000077500000000000000000000000001474173014300250065ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/testdata/jwks_missing_kid.json000066400000000000000000000007371474173014300312460ustar00rootroot00000000000000{ "keys": [ { "kty": "EC", "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" }, { "kty": "EC", "crv": "P-256", "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY", "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/testdata/jwks_valid_1.json000066400000000000000000000004261474173014300302600ustar00rootroot00000000000000{ "keys": [ { "kty": "EC", "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/jwtbundle/testdata/jwks_valid_2.json000066400000000000000000000010261474173014300302560ustar00rootroot00000000000000{ "keys": [ { "kty": "EC", "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" }, { "kty": "EC", "kid": "gHTCunJbefYtnZnTctd84xeRWyMrEsWD", "crv": "P-256", "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY", "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/000077500000000000000000000000001474173014300236455ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/bundle.go000066400000000000000000000337121474173014300254530ustar00rootroot00000000000000package spiffebundle import ( "crypto" "crypto/x509" "encoding/json" "io" "os" "sync" "time" "github.com/go-jose/go-jose/v4" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/jwtutil" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) const ( x509SVIDUse = "x509-svid" jwtSVIDUse = "jwt-svid" ) var spiffebundleErr = errs.Class("spiffebundle") type bundleDoc struct { jose.JSONWebKeySet SequenceNumber *uint64 `json:"spiffe_sequence,omitempty"` RefreshHint *int64 `json:"spiffe_refresh_hint,omitempty"` } // Bundle is a collection of trusted public key material for a trust domain, // conforming to the SPIFFE Bundle Format as part of the SPIFFE Trust Domain // and Bundle specification: // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md type Bundle struct { trustDomain spiffeid.TrustDomain mtx sync.RWMutex refreshHint *time.Duration sequenceNumber *uint64 jwtAuthorities map[string]crypto.PublicKey x509Authorities []*x509.Certificate } // New creates a new bundle. func New(trustDomain spiffeid.TrustDomain) *Bundle { return &Bundle{ trustDomain: trustDomain, jwtAuthorities: make(map[string]crypto.PublicKey), } } // Load loads a bundle from a file on disk. The file must contain a JWKS // document following the SPIFFE Trust Domain and Bundle specification. func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) { bundleBytes, err := os.ReadFile(path) if err != nil { return nil, spiffebundleErr.New("unable to read SPIFFE bundle: %w", err) } return Parse(trustDomain, bundleBytes) } // Read decodes a bundle from a reader. The contents must contain a JWKS // document following the SPIFFE Trust Domain and Bundle specification. func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) { b, err := io.ReadAll(r) if err != nil { return nil, spiffebundleErr.New("unable to read: %v", err) } return Parse(trustDomain, b) } // Parse parses a bundle from bytes. The data must be a JWKS document following // the SPIFFE Trust Domain and Bundle specification. func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) { jwks := &bundleDoc{} if err := json.Unmarshal(bundleBytes, jwks); err != nil { return nil, spiffebundleErr.New("unable to parse JWKS: %v", err) } bundle := New(trustDomain) if jwks.RefreshHint != nil { bundle.SetRefreshHint(time.Second * time.Duration(*jwks.RefreshHint)) } if jwks.SequenceNumber != nil { bundle.SetSequenceNumber(*jwks.SequenceNumber) } if jwks.Keys == nil { // The parameter keys MUST be present. // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#413-keys return nil, spiffebundleErr.New("no authorities found") } for i, key := range jwks.Keys { switch key.Use { // Two SVID types are supported: x509-svid and jwt-svid. case x509SVIDUse: if len(key.Certificates) != 1 { return nil, spiffebundleErr.New("expected a single certificate in %s entry %d; got %d", x509SVIDUse, i, len(key.Certificates)) } bundle.AddX509Authority(key.Certificates[0]) case jwtSVIDUse: if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil { return nil, spiffebundleErr.New("error adding authority %d of JWKS: %v", i, errs.Unwrap(err)) } } } return bundle, nil } // FromX509Bundle creates a bundle from an X.509 bundle. // The function panics in case of a nil X.509 bundle. func FromX509Bundle(x509Bundle *x509bundle.Bundle) *Bundle { bundle := New(x509Bundle.TrustDomain()) bundle.x509Authorities = x509Bundle.X509Authorities() return bundle } // FromJWTBundle creates a bundle from a JWT bundle. // The function panics in case of a nil JWT bundle. func FromJWTBundle(jwtBundle *jwtbundle.Bundle) *Bundle { bundle := New(jwtBundle.TrustDomain()) bundle.jwtAuthorities = jwtBundle.JWTAuthorities() return bundle } // FromX509Authorities creates a bundle from X.509 certificates. func FromX509Authorities(trustDomain spiffeid.TrustDomain, x509Authorities []*x509.Certificate) *Bundle { bundle := New(trustDomain) bundle.x509Authorities = x509util.CopyX509Authorities(x509Authorities) return bundle } // FromJWTAuthorities creates a new bundle from JWT authorities. func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle { bundle := New(trustDomain) bundle.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities) return bundle } // TrustDomain returns the trust domain that the bundle belongs to. func (b *Bundle) TrustDomain() spiffeid.TrustDomain { return b.trustDomain } // X509Authorities returns the X.509 authorities in the bundle. func (b *Bundle) X509Authorities() []*x509.Certificate { b.mtx.RLock() defer b.mtx.RUnlock() return x509util.CopyX509Authorities(b.x509Authorities) } // AddX509Authority adds an X.509 authority to the bundle. If the authority already // exists in the bundle, the contents of the bundle will remain unchanged. func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() for _, r := range b.x509Authorities { if r.Equal(x509Authority) { return } } b.x509Authorities = append(b.x509Authorities, x509Authority) } // RemoveX509Authority removes an X.509 authority from the bundle. func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() for i, r := range b.x509Authorities { if r.Equal(x509Authority) { b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...) return } } } // HasX509Authority checks if the given X.509 authority exists in the bundle. func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool { b.mtx.RLock() defer b.mtx.RUnlock() for _, r := range b.x509Authorities { if r.Equal(x509Authority) { return true } } return false } // SetX509Authorities sets the X.509 authorities in the bundle. func (b *Bundle) SetX509Authorities(authorities []*x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() b.x509Authorities = x509util.CopyX509Authorities(authorities) } // JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID. func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey { b.mtx.RLock() defer b.mtx.RUnlock() return jwtutil.CopyJWTAuthorities(b.jwtAuthorities) } // FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority // is found, it is returned and the boolean is true. Otherwise, the returned // value is nil and the boolean is false. func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) { b.mtx.RLock() defer b.mtx.RUnlock() jwtAuthority, ok := b.jwtAuthorities[keyID] return jwtAuthority, ok } // HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID. func (b *Bundle) HasJWTAuthority(keyID string) bool { b.mtx.RLock() defer b.mtx.RUnlock() _, ok := b.jwtAuthorities[keyID] return ok } // AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists // under the given key ID, it is replaced. A key ID must be specified. func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error { if keyID == "" { return spiffebundleErr.New("keyID cannot be empty") } b.mtx.Lock() defer b.mtx.Unlock() b.jwtAuthorities[keyID] = jwtAuthority return nil } // RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle. func (b *Bundle) RemoveJWTAuthority(keyID string) { b.mtx.Lock() defer b.mtx.Unlock() delete(b.jwtAuthorities, keyID) } // SetJWTAuthorities sets the JWT authorities in the bundle. func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) { b.mtx.Lock() defer b.mtx.Unlock() b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities) } // Empty returns true if the bundle has no X.509 and JWT authorities. func (b *Bundle) Empty() bool { b.mtx.RLock() defer b.mtx.RUnlock() return len(b.x509Authorities) == 0 && len(b.jwtAuthorities) == 0 } // RefreshHint returns the refresh hint. If the refresh hint is set in // the bundle, it is returned and the boolean is true. Otherwise, the returned // value is zero and the boolean is false. func (b *Bundle) RefreshHint() (refreshHint time.Duration, ok bool) { b.mtx.RLock() defer b.mtx.RUnlock() if b.refreshHint != nil { return *b.refreshHint, true } return 0, false } // SetRefreshHint sets the refresh hint. The refresh hint value will be // truncated to time.Second. func (b *Bundle) SetRefreshHint(refreshHint time.Duration) { b.mtx.Lock() defer b.mtx.Unlock() b.refreshHint = &refreshHint } // ClearRefreshHint clears the refresh hint. func (b *Bundle) ClearRefreshHint() { b.mtx.Lock() defer b.mtx.Unlock() b.refreshHint = nil } // SequenceNumber returns the sequence number. If the sequence number is set in // the bundle, it is returned and the boolean is true. Otherwise, the returned // value is zero and the boolean is false. func (b *Bundle) SequenceNumber() (uint64, bool) { b.mtx.RLock() defer b.mtx.RUnlock() if b.sequenceNumber != nil { return *b.sequenceNumber, true } return 0, false } // SetSequenceNumber sets the sequence number. func (b *Bundle) SetSequenceNumber(sequenceNumber uint64) { b.mtx.Lock() defer b.mtx.Unlock() b.sequenceNumber = &sequenceNumber } // ClearSequenceNumber clears the sequence number. func (b *Bundle) ClearSequenceNumber() { b.mtx.Lock() defer b.mtx.Unlock() b.sequenceNumber = nil } // Marshal marshals the bundle according to the SPIFFE Trust Domain and Bundle // specification. The trust domain is not marshaled as part of the bundle and // must be conveyed separately. See the specification for details. func (b *Bundle) Marshal() ([]byte, error) { b.mtx.RLock() defer b.mtx.RUnlock() jwks := bundleDoc{} if b.refreshHint != nil { tr := int64((*b.refreshHint + (time.Second - 1)) / time.Second) jwks.RefreshHint = &tr } jwks.SequenceNumber = b.sequenceNumber for _, x509Authority := range b.x509Authorities { jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ Key: x509Authority.PublicKey, Certificates: []*x509.Certificate{x509Authority}, Use: x509SVIDUse, }) } for keyID, jwtAuthority := range b.jwtAuthorities { jwks.Keys = append(jwks.Keys, jose.JSONWebKey{ Key: jwtAuthority, KeyID: keyID, Use: jwtSVIDUse, }) } return json.Marshal(jwks) } // Clone clones the bundle. func (b *Bundle) Clone() *Bundle { b.mtx.RLock() defer b.mtx.RUnlock() return &Bundle{ trustDomain: b.trustDomain, refreshHint: copyRefreshHint(b.refreshHint), sequenceNumber: copySequenceNumber(b.sequenceNumber), x509Authorities: x509util.CopyX509Authorities(b.x509Authorities), jwtAuthorities: jwtutil.CopyJWTAuthorities(b.jwtAuthorities), } } // X509Bundle returns an X.509 bundle containing the X.509 authorities in the SPIFFE // bundle. func (b *Bundle) X509Bundle() *x509bundle.Bundle { b.mtx.RLock() defer b.mtx.RUnlock() // FromX509Authorities makes a copy, so we can pass our internal slice directly. return x509bundle.FromX509Authorities(b.trustDomain, b.x509Authorities) } // JWTBundle returns a JWT bundle containing the JWT authorities in the SPIFFE bundle. func (b *Bundle) JWTBundle() *jwtbundle.Bundle { b.mtx.RLock() defer b.mtx.RUnlock() // FromJWTBundle makes a copy, so we can pass our internal slice directly. return jwtbundle.FromJWTAuthorities(b.trustDomain, b.jwtAuthorities) } // GetBundleForTrustDomain returns the SPIFFE bundle for the given trust // domain. It implements the Source interface. An error will be returned if the // trust domain does not match that of the bundle. func (b *Bundle) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { b.mtx.RLock() defer b.mtx.RUnlock() if b.trustDomain != trustDomain { return nil, spiffebundleErr.New("no SPIFFE bundle for trust domain %q", trustDomain) } return b, nil } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the x509bundle.Source interface. An error will be // returned if the trust domain does not match that of the bundle. func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { b.mtx.RLock() defer b.mtx.RUnlock() if b.trustDomain != trustDomain { return nil, spiffebundleErr.New("no X.509 bundle for trust domain %q", trustDomain) } return b.X509Bundle(), nil } // GetJWTBundleForTrustDomain returns the JWT bundle of the given trust domain. // It implements the jwtbundle.Source interface. An error will be returned if // the trust domain does not match that of the bundle. func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { b.mtx.RLock() defer b.mtx.RUnlock() if b.trustDomain != trustDomain { return nil, spiffebundleErr.New("no JWT bundle for trust domain %q", trustDomain) } return b.JWTBundle(), nil } // Equal compares the bundle for equality against the given bundle. func (b *Bundle) Equal(other *Bundle) bool { if b == nil || other == nil { return b == other } return b.trustDomain == other.trustDomain && refreshHintEqual(b.refreshHint, other.refreshHint) && sequenceNumberEqual(b.sequenceNumber, other.sequenceNumber) && jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) && x509util.CertsEqual(b.x509Authorities, other.x509Authorities) } func refreshHintEqual(a, b *time.Duration) bool { if a == nil || b == nil { return a == b } return *a == *b } func sequenceNumberEqual(a, b *uint64) bool { if a == nil || b == nil { return a == b } return *a == *b } func copyRefreshHint(refreshHint *time.Duration) *time.Duration { if refreshHint == nil { return nil } copied := *refreshHint return &copied } func copySequenceNumber(sequenceNumber *uint64) *uint64 { if sequenceNumber == nil { return nil } copied := *sequenceNumber return &copied } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/bundle_test.go000066400000000000000000000316661474173014300265200ustar00rootroot00000000000000package spiffebundle_test import ( "crypto" "crypto/x509" "os" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/errstrings" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testCase struct { filePath, err string refreshHint time.Duration sequenceNumber uint64 keysCount int } var ( td = spiffeid.RequireTrustDomainFromString("domain.test") td2 = spiffeid.RequireTrustDomainFromString("domain2.test") x509Cert1 = &x509.Certificate{ Raw: []byte("CERT 1"), } x509Cert2 = &x509.Certificate{ Raw: []byte("CERT 2"), } testCases = []testCase{ { filePath: "testdata/does-not-exist.json", }, { filePath: "testdata/spiffebundle_valid_1.json", keysCount: 1, }, { filePath: "testdata/spiffebundle_valid_2.json", keysCount: 6, refreshHint: 60 * time.Second, sequenceNumber: 1, }, { filePath: "testdata/spiffebundle_missing_kid.json", err: "spiffebundle: error adding authority 1 of JWKS: keyID cannot be empty", }, { filePath: "testdata/spiffebundle_no_keys.json", err: "spiffebundle: no authorities found", }, { filePath: "testdata/spiffebundle_multiple_x509.json", err: "spiffebundle: expected a single certificate in x509-svid entry 0; got 2", }, } ) func TestNew(t *testing.T) { b := spiffebundle.New(td) require.NotNil(t, b) require.Len(t, b.JWTAuthorities(), 0) require.Equal(t, td, b.TrustDomain()) } func TestLoad(t *testing.T) { testCases[0].err = "spiffebundle: unable to read SPIFFE bundle: open testdata/does-not-exist.json: " + errstrings.FileNotFound for _, testCase := range testCases { testCase := testCase t.Run(testCase.filePath, func(t *testing.T) { bundle, err := spiffebundle.Load(td, testCase.filePath) checkBundleProperties(t, err, testCase, bundle) }) } } func TestRead(t *testing.T) { testCases[0].err = "spiffebundle: unable to read: invalid argument" for _, testCase := range testCases { testCase := testCase t.Run(testCase.filePath, func(t *testing.T) { // we expect the Open call to fail in some cases file, _ := os.Open(testCase.filePath) defer file.Close() bundle, err := spiffebundle.Read(td, file) checkBundleProperties(t, err, testCase, bundle) }) } } func TestParse(t *testing.T) { testCases[0].err = "spiffebundle: unable to parse JWKS: unexpected end of JSON input" for _, testCase := range testCases { testCase := testCase t.Run(testCase.filePath, func(t *testing.T) { // we expect the ReadFile call to fail in some cases bundleBytes, _ := os.ReadFile(testCase.filePath) bundle, err := spiffebundle.Parse(td, bundleBytes) checkBundleProperties(t, err, testCase, bundle) }) } } func TestFromX509Bundle(t *testing.T) { xb := x509bundle.FromX509Authorities(td, []*x509.Certificate{x509Cert1}) sb := spiffebundle.FromX509Bundle(xb) require.NotNil(t, sb) assert.Equal(t, xb.X509Authorities(), sb.X509Authorities()) } func TestFromJWTBundle(t *testing.T) { jb := jwtbundle.New(td) err := jb.AddJWTAuthority("key-1", "test-1") require.NoError(t, err) sb := spiffebundle.FromJWTBundle(jb) require.NotNil(t, sb) assert.Equal(t, jb.JWTAuthorities(), sb.JWTAuthorities()) } func TestFromX509Authorities(t *testing.T) { x509Authorities := []*x509.Certificate{x509Cert1, x509Cert2} b := spiffebundle.FromX509Authorities(td, x509Authorities) require.NotNil(t, b) assert.Equal(t, b.X509Authorities(), x509Authorities) } func TestFromJWTAuthorities(t *testing.T) { jwtAuthorities := map[string]crypto.PublicKey{ "key-1": "test-1", "key-2": "test-2", } b := spiffebundle.FromJWTAuthorities(td, jwtAuthorities) require.NotNil(t, b) assert.Equal(t, b.JWTAuthorities(), jwtAuthorities) } func TestTrustDomain(t *testing.T) { b := spiffebundle.New(td) btd := b.TrustDomain() require.Equal(t, td, btd) } func TestJWTAuthoritiesCRUD(t *testing.T) { // Test AddJWTAuthority (missing authority) b := spiffebundle.New(td) err := b.AddJWTAuthority("", "test-1") require.EqualError(t, err, "spiffebundle: keyID cannot be empty") // Test AddJWTAuthority (new authority) err = b.AddJWTAuthority("key-1", "test-1") require.NoError(t, err) // Test JWTAuthorities jwtAuthorities := b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{"key-1": "test-1"}, jwtAuthorities) err = b.AddJWTAuthority("key-2", "test-2") require.NoError(t, err) jwtAuthorities = b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{ "key-1": "test-1", "key-2": "test-2", }, jwtAuthorities) // Test FindJWTAuthority jwtAuthority, ok := b.FindJWTAuthority("key-1") require.True(t, ok) require.Equal(t, "test-1", jwtAuthority) jwtAuthority, ok = b.FindJWTAuthority("key-3") require.Nil(t, jwtAuthority) require.False(t, ok) require.Equal(t, true, b.HasJWTAuthority("key-1")) b.RemoveJWTAuthority("key-3") require.Equal(t, 2, len(b.JWTAuthorities())) require.Equal(t, true, b.HasJWTAuthority("key-1")) require.Equal(t, true, b.HasJWTAuthority("key-2")) // Test RemoveJWTAuthority b.RemoveJWTAuthority("key-2") require.Equal(t, 1, len(b.JWTAuthorities())) require.Equal(t, true, b.HasJWTAuthority("key-1")) // Test AddJWTAuthority (update authority) err = b.AddJWTAuthority("key-1", "test-1-updated") require.NoError(t, err) jwtAuthorities = b.JWTAuthorities() require.Equal(t, map[string]crypto.PublicKey{ "key-1": "test-1-updated", }, jwtAuthorities) } func TestX509AuthoritiesCRUD(t *testing.T) { // Test X509Authorities and HasX509Authority b := spiffebundle.New(td) require.Len(t, b.X509Authorities(), 0) require.Equal(t, false, b.HasX509Authority(x509Cert1)) // Test AddX509Authority b.AddX509Authority(x509Cert1) require.Len(t, b.X509Authorities(), 1) require.Equal(t, true, b.HasX509Authority(x509Cert1)) b.AddX509Authority(x509Cert1) require.Len(t, b.X509Authorities(), 1) require.Equal(t, true, b.HasX509Authority(x509Cert1)) b.AddX509Authority(x509Cert2) require.Len(t, b.X509Authorities(), 2) require.Equal(t, true, b.HasX509Authority(x509Cert2)) // Test RemoveX509Authority b.RemoveX509Authority(x509Cert1) require.Len(t, b.X509Authorities(), 1) require.Equal(t, true, b.HasX509Authority(x509Cert2)) b.RemoveX509Authority(x509Cert2) require.Len(t, b.X509Authorities(), 0) } func TestRefreshHint(t *testing.T) { b := spiffebundle.New(td) rh, ok := b.RefreshHint() assert.Equal(t, false, ok) assert.Equal(t, time.Duration(0), rh) b.SetRefreshHint(30 * time.Second) rh, ok = b.RefreshHint() assert.Equal(t, true, ok) assert.Equal(t, 30*time.Second, rh) b.ClearRefreshHint() rh, ok = b.RefreshHint() assert.Equal(t, false, ok) assert.Equal(t, time.Duration(0), rh) } func TestSequenceNumber(t *testing.T) { b := spiffebundle.New(td) sn, ok := b.SequenceNumber() assert.Equal(t, false, ok) assert.Equal(t, uint64(0), sn) b.SetSequenceNumber(5) sn, ok = b.SequenceNumber() assert.Equal(t, true, ok) assert.Equal(t, uint64(5), sn) b.ClearSequenceNumber() sn, ok = b.SequenceNumber() assert.Equal(t, false, ok) assert.Equal(t, uint64(0), sn) } func TestMarshal(t *testing.T) { // Load a bundle to marshal bundle, err := spiffebundle.Load(td, "testdata/spiffebundle_valid_2.json") require.NoError(t, err) // Marshal the bundle bundleBytesMarshal, err := bundle.Marshal() require.NoError(t, err) // Parse the marshaled bundle bundleParsed, err := spiffebundle.Parse(td, bundleBytesMarshal) require.NoError(t, err) // Assert that the marshaled bundle is equal to the parsed bundle assert.Equal(t, bundleParsed, bundle) } func TestX509Bundle(t *testing.T) { sb := spiffebundle.New(td) sb.AddX509Authority(x509Cert1) xb := sb.X509Bundle() require.Equal(t, true, xb.HasX509Authority(x509Cert1)) } func TestJWTBundle(t *testing.T) { sb := spiffebundle.New(td) err := sb.AddJWTAuthority("key-1", "test-1") require.NoError(t, err) jb := sb.JWTBundle() require.Equal(t, true, jb.HasJWTAuthority("key-1")) } func TestGetBundleForTrustDomain(t *testing.T) { b := spiffebundle.New(td) b1, err := b.GetBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, b, b1) b2, err := b.GetBundleForTrustDomain(td2) require.Nil(t, b2) require.EqualError(t, err, `spiffebundle: no SPIFFE bundle for trust domain "domain2.test"`) } func TestGetX509BundleForTrustDomain(t *testing.T) { xb1 := x509bundle.FromX509Authorities(td, []*x509.Certificate{x509Cert1, x509Cert2}) sb := spiffebundle.FromX509Bundle(xb1) xb2, err := sb.GetX509BundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, xb1, xb2) xb2, err = sb.GetX509BundleForTrustDomain(td2) require.Nil(t, xb2) require.EqualError(t, err, `spiffebundle: no X.509 bundle for trust domain "domain2.test"`) } func TestGetJWTBundleForTrustDomain(t *testing.T) { jb1 := jwtbundle.FromJWTAuthorities(td, map[string]crypto.PublicKey{"key-1": "test-1"}) sb := spiffebundle.FromJWTBundle(jb1) jb2, err := sb.GetJWTBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, jb1, jb2) jb2, err = sb.GetJWTBundleForTrustDomain(td2) require.Nil(t, jb2) require.EqualError(t, err, `spiffebundle: no JWT bundle for trust domain "domain2.test"`) } func TestEqual(t *testing.T) { ca1 := test.NewCA(t, td) ca2 := test.NewCA(t, td2) empty := spiffebundle.New(td) empty2 := spiffebundle.New(td2) refreshHint1 := spiffebundle.New(td) refreshHint1.SetRefreshHint(time.Second) refreshHint2 := spiffebundle.New(td) refreshHint2.SetRefreshHint(time.Second * 2) sequenceNumber1 := spiffebundle.New(td) sequenceNumber1.SetSequenceNumber(1) sequenceNumber2 := spiffebundle.New(td) sequenceNumber2.SetSequenceNumber(2) x509Authorities1 := spiffebundle.FromX509Authorities(td, ca1.X509Authorities()) x509Authorities2 := spiffebundle.FromX509Authorities(td, ca2.X509Authorities()) jwtAuthorities1 := spiffebundle.FromJWTAuthorities(td, ca1.JWTAuthorities()) jwtAuthorities2 := spiffebundle.FromJWTAuthorities(td, ca2.JWTAuthorities()) for _, tt := range []struct { name string a *spiffebundle.Bundle b *spiffebundle.Bundle expectEqual bool }{ { name: "empty equal", a: empty, b: empty, expectEqual: true, }, { name: "different trust domains", a: empty, b: empty2, expectEqual: false, }, { name: "refresh hint equal", a: refreshHint1, b: refreshHint1, expectEqual: true, }, { name: "refresh hint nil and non-nil", a: empty, b: refreshHint1, expectEqual: false, }, { name: "refresh hint non-nil but not equal", a: refreshHint1, b: refreshHint2, expectEqual: false, }, { name: "sequence number equal", a: sequenceNumber1, b: sequenceNumber1, expectEqual: true, }, { name: "sequence number nil and non-nil", a: empty, b: sequenceNumber1, expectEqual: false, }, { name: "sequence number non-nil but not equal", a: sequenceNumber1, b: sequenceNumber2, expectEqual: false, }, { name: "X509 authorities equal", a: x509Authorities1, b: x509Authorities1, expectEqual: true, }, { name: "X509 authorities empty and not empty", a: empty, b: x509Authorities1, expectEqual: false, }, { name: "X509 authorities not empty but not equal", a: x509Authorities1, b: x509Authorities2, expectEqual: false, }, { name: "JWT authorities equal", a: jwtAuthorities1, b: jwtAuthorities1, expectEqual: true, }, { name: "JWT authorities empty and not empty", a: empty, b: jwtAuthorities1, expectEqual: false, }, { name: "JWT authorities not empty but not equal", a: jwtAuthorities1, b: jwtAuthorities2, expectEqual: false, }, } { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.expectEqual, tt.a.Equal(tt.b)) }) } } func TestClone(t *testing.T) { // Load a bundle to clone original, err := spiffebundle.Load(td, "testdata/spiffebundle_valid_2.json") require.NoError(t, err) cloned := original.Clone() require.True(t, original.Equal(cloned)) } func checkBundleProperties(t *testing.T, err error, tc testCase, b *spiffebundle.Bundle) { if tc.err != "" { require.EqualError(t, err, tc.err) return } require.NoError(t, err) require.NotNil(t, b) assert.Len(t, b.JWTAuthorities(), tc.keysCount) rh, ok := b.RefreshHint() if tc.refreshHint > 0 { assert.Equal(t, true, ok) assert.Equal(t, tc.refreshHint, rh) } sn, ok := b.SequenceNumber() if tc.sequenceNumber > 0 { assert.Equal(t, true, ok) assert.Equal(t, tc.sequenceNumber, sn) } } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/doc.go000066400000000000000000000037301474173014300247440ustar00rootroot00000000000000// Package spiffebundle provides SPIFFE bundle related functionality. // // A bundle represents a SPIFFE bundle, a collection authorities for // authenticating SVIDs. // // You can create a new bundle for a specific trust domain: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := spiffebundle.New(td) // // Or you can load it from disk: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := spiffebundle.Load(td, "bundle.json") // // The bundle can be initialized with X.509 or JWT authorities: // // td := spiffeid.RequireTrustDomain("example.org") // // var x509Authorities []*x509.Certificate = ... // bundle := spiffebundle.FromX509Authorities(td, x509Authorities) // // ... or ... // var jwtAuthorities map[string]crypto.PublicKey = ... // bundle := spiffebundle.FromJWTAuthorities(td, jwtAuthorities) // // In addition, you can add authorities to the bundle: // // var x509CA *x509.Certificate = ... // bundle.AddX509Authority(x509CA) // var keyID string = ... // var publicKey crypto.PublicKey = ... // bundle.AddJWTAuthority(keyID, publicKey) // // Bundles can be organized into a set, keyed by trust domain: // // set := spiffebundle.NewSet() // set.Add(bundle) // // A Source is source of bundles for a trust domain. Both the // Bundle and Set types implement Source: // // // Initialize the source from a bundle or set // var source spiffebundle.Source = bundle // // ... or ... // var source spiffebundle.Source = set // // // Use the source to query for X.509 bundles by trust domain // bundle, err := source.GetBundleForTrustDomain(td) // // Additionally the Bundle and Set types also implement the x509bundle.Source and jwtbundle.Source interfaces: // // // As an x509bundle.Source... // var source x509bundle.Source = bundle // or set // x509Bundle, err := source.GetX509BundleForTrustDomain(td) // // // As a jwtbundle.Source... // var source jwtbundle.Source = bundle // or set // jwtBundle, err := source.GetJWTBundleForTrustDomain(td) package spiffebundle golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/set.go000066400000000000000000000066071474173014300250000ustar00rootroot00000000000000package spiffebundle import ( "sort" "sync" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Set is a set of bundles, keyed by trust domain. type Set struct { mtx sync.RWMutex bundles map[spiffeid.TrustDomain]*Bundle } // NewSet creates a new set initialized with the given bundles. func NewSet(bundles ...*Bundle) *Set { bundlesMap := make(map[spiffeid.TrustDomain]*Bundle) for _, b := range bundles { if b != nil { bundlesMap[b.trustDomain] = b } } return &Set{ bundles: bundlesMap, } } // Add adds a new bundle into the set. If a bundle already exists for the // trust domain, the existing bundle is replaced. func (s *Set) Add(bundle *Bundle) { s.mtx.Lock() defer s.mtx.Unlock() if bundle != nil { s.bundles[bundle.trustDomain] = bundle } } // Remove removes the bundle for the given trust domain. func (s *Set) Remove(trustDomain spiffeid.TrustDomain) { s.mtx.Lock() defer s.mtx.Unlock() delete(s.bundles, trustDomain) } // Has returns true if there is a bundle for the given trust domain. func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool { s.mtx.RLock() defer s.mtx.RUnlock() _, ok := s.bundles[trustDomain] return ok } // Get returns a bundle for the given trust domain. If the bundle is in the set // it is returned and the boolean is true. Otherwise, the returned value is // nil and the boolean is false. func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] return bundle, ok } // Bundles returns the bundles in the set sorted by trust domain. func (s *Set) Bundles() []*Bundle { s.mtx.RLock() defer s.mtx.RUnlock() out := make([]*Bundle, 0, len(s.bundles)) for _, bundle := range s.bundles { out = append(out, bundle) } sort.Slice(out, func(a, b int) bool { return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0 }) return out } // Len returns the number of bundles in the set. func (s *Set) Len() int { s.mtx.RLock() defer s.mtx.RUnlock() return len(s.bundles) } // GetBundleForTrustDomain returns the SPIFFE bundle for the given trust // domain. It implements the Source interface. func (s *Set) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] if !ok { return nil, spiffebundleErr.New("no SPIFFE bundle for trust domain %q", trustDomain) } return bundle, nil } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the x509bundle.Source interface. func (s *Set) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] if !ok { return nil, spiffebundleErr.New("no X.509 bundle for trust domain %q", trustDomain) } return bundle.X509Bundle(), nil } // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. It implements the jwtbundle.Source interface. func (s *Set) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] if !ok { return nil, spiffebundleErr.New("no JWT bundle for trust domain %q", trustDomain) } return bundle.JWTBundle(), nil } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/set_test.go000066400000000000000000000042771474173014300260400ustar00rootroot00000000000000package spiffebundle_test import ( "crypto" "crypto/x509" "testing" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/stretchr/testify/require" ) var ( b1 = spiffebundle.New(td) ) func TestNewSet(t *testing.T) { s := spiffebundle.NewSet(b1) require.True(t, s.Has(td)) s = spiffebundle.NewSet(spiffebundle.New(td), spiffebundle.New(td2)) require.True(t, s.Has(td)) require.True(t, s.Has(td2)) } func TestAdd(t *testing.T) { s := spiffebundle.NewSet() require.False(t, s.Has(td)) s.Add(b1) require.True(t, s.Has(td)) } func TestRemove(t *testing.T) { s := spiffebundle.NewSet(b1) require.True(t, s.Has(td)) s.Remove(td2) require.True(t, s.Has(td)) s.Remove(td) require.False(t, s.Has(td)) } func TestHas(t *testing.T) { s := spiffebundle.NewSet(spiffebundle.New(td)) require.False(t, s.Has(td2)) require.True(t, s.Has(td)) } func TestSetGetBundleForTrustDomain(t *testing.T) { s := spiffebundle.NewSet(b1) _, err := s.GetBundleForTrustDomain(td2) require.EqualError(t, err, `spiffebundle: no SPIFFE bundle for trust domain "domain2.test"`) b, err := s.GetBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, b1, b) } func TestSetGetX509BundleForTrustDomain(t *testing.T) { xb1 := x509bundle.FromX509Authorities(td, []*x509.Certificate{x509Cert1}) b := spiffebundle.FromX509Bundle(xb1) s := spiffebundle.NewSet(b) _, err := s.GetX509BundleForTrustDomain(td2) require.EqualError(t, err, `spiffebundle: no X.509 bundle for trust domain "domain2.test"`) xb2, err := s.GetX509BundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, xb1, xb2) } func TestSetGetJWTBundleForTrustDomain(t *testing.T) { jwtAuthorities := map[string]crypto.PublicKey{ "key-1": "test-1", "key-2": "test-2", } jb1 := jwtbundle.FromJWTAuthorities(td, jwtAuthorities) b := spiffebundle.FromJWTBundle(jb1) s := spiffebundle.NewSet(b) _, err := s.GetJWTBundleForTrustDomain(td2) require.EqualError(t, err, `spiffebundle: no JWT bundle for trust domain "domain2.test"`) jb2, err := s.GetJWTBundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, jb1, jb2) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/source.go000066400000000000000000000005131474173014300254730ustar00rootroot00000000000000package spiffebundle import "github.com/spiffe/go-spiffe/v2/spiffeid" // Source represents a source of SPIFFE bundles keyed by trust domain. type Source interface { // GetBundleForTrustDomain returns the SPIFFE bundle for the given trust // domain. GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/000077500000000000000000000000001474173014300254565ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/spiffebundle_missing_kid.json000066400000000000000000000022641474173014300334030ustar00rootroot00000000000000{ "keys": [ { "use": "x509-svid", "kty": "EC", "crv": "P-384", "x": "WjB-nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0", "y": "Z-0_tDH_r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs_mcmvPqVK9j", "x5c": [ "MIIBzDCCAVOgAwIBAgIJAJM4DhRH0vmuMAoGCCqGSM49BAMEMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMTgwNTEzMTkzMzQ3WhcNMjMwNTEyMTkzMzQ3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWjB+nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0Z+0/tDH/r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs/mcmvPqVK9jo10wWzAdBgNVHQ4EFgQUh6XzV6LwNazA+GTEVOdu07o5yOgwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwGQYDVR0RBBIwEIYOc3BpZmZlOi8vbG9jYWwwCgYIKoZIzj0EAwQDZwAwZAIwE4Me13qMC9i6Fkx0h26y09QZIbuRqA9puLg9AeeAAyo5tBzRl1YL0KNEp02VKSYJAjBdeJvqjJ9wW55OGj1JQwDFD7kWeEB6oMlwPbI/5hEY3azJi16I0uN1JSYTSWGSqWc=" ] }, { "use": "jwt-svid", "kty": "EC", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/spiffebundle_multiple_x509.json000066400000000000000000000035531474173014300335250ustar00rootroot00000000000000{ "keys": [ { "use": "x509-svid", "kty": "EC", "crv": "P-384", "x": "WjB-nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0", "y": "Z-0_tDH_r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs_mcmvPqVK9j", "x5c": [ "MIIBzDCCAVOgAwIBAgIJAJM4DhRH0vmuMAoGCCqGSM49BAMEMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMTgwNTEzMTkzMzQ3WhcNMjMwNTEyMTkzMzQ3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWjB+nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0Z+0/tDH/r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs/mcmvPqVK9jo10wWzAdBgNVHQ4EFgQUh6XzV6LwNazA+GTEVOdu07o5yOgwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwGQYDVR0RBBIwEIYOc3BpZmZlOi8vbG9jYWwwCgYIKoZIzj0EAwQDZwAwZAIwE4Me13qMC9i6Fkx0h26y09QZIbuRqA9puLg9AeeAAyo5tBzRl1YL0KNEp02VKSYJAjBdeJvqjJ9wW55OGj1JQwDFD7kWeEB6oMlwPbI/5hEY3azJi16I0uN1JSYTSWGSqWc=", "MIIBzDCCAVOgAwIBAgIJAJM4DhRH0vmuMAoGCCqGSM49BAMEMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMTgwNTEzMTkzMzQ3WhcNMjMwNTEyMTkzMzQ3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWjB+nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0Z+0/tDH/r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs/mcmvPqVK9jo10wWzAdBgNVHQ4EFgQUh6XzV6LwNazA+GTEVOdu07o5yOgwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwGQYDVR0RBBIwEIYOc3BpZmZlOi8vbG9jYWwwCgYIKoZIzj0EAwQDZwAwZAIwE4Me13qMC9i6Fkx0h26y09QZIbuRqA9puLg9AeeAAyo5tBzRl1YL0KNEp02VKSYJAjBdeJvqjJ9wW55OGj1JQwDFD7kWeEB6oMlwPbI/5hEY3azJi16I0uN1JSYTSWGSqWc=" ] }, { "use": "jwt-svid", "kty": "EC", "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/spiffebundle_no_keys.json000066400000000000000000000000731474173014300325460ustar00rootroot00000000000000{ "spiffe_refresh_hint": 60, "spiffe_sequence": 1 }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/spiffebundle_valid_1.json000066400000000000000000000023531474173014300324210ustar00rootroot00000000000000{ "keys": [ { "use": "x509-svid", "kty": "EC", "crv": "P-384", "x": "WjB-nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0", "y": "Z-0_tDH_r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs_mcmvPqVK9j", "x5c": [ "MIIBzDCCAVOgAwIBAgIJAJM4DhRH0vmuMAoGCCqGSM49BAMEMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMTgwNTEzMTkzMzQ3WhcNMjMwNTEyMTkzMzQ3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWjB+nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0Z+0/tDH/r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs/mcmvPqVK9jo10wWzAdBgNVHQ4EFgQUh6XzV6LwNazA+GTEVOdu07o5yOgwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwGQYDVR0RBBIwEIYOc3BpZmZlOi8vbG9jYWwwCgYIKoZIzj0EAwQDZwAwZAIwE4Me13qMC9i6Fkx0h26y09QZIbuRqA9puLg9AeeAAyo5tBzRl1YL0KNEp02VKSYJAjBdeJvqjJ9wW55OGj1JQwDFD7kWeEB6oMlwPbI/5hEY3azJi16I0uN1JSYTSWGSqWc=" ] }, { "use": "jwt-svid", "kty": "EC", "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD", "crv": "P-256", "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k", "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM" } ] }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/spiffebundle/testdata/spiffebundle_valid_2.json000066400000000000000000000052771474173014300324320ustar00rootroot00000000000000{ "keys": [ { "use": "x509-svid", "kty": "EC", "crv": "P-384", "x": "WjB-nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0", "y": "Z-0_tDH_r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs_mcmvPqVK9j", "x5c": [ "MIIBzDCCAVOgAwIBAgIJAJM4DhRH0vmuMAoGCCqGSM49BAMEMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMTgwNTEzMTkzMzQ3WhcNMjMwNTEyMTkzMzQ3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWjB+nSGSxIYiznb84xu5WGDZj80nL7W1c3zf48Why0ma7Y7mCBKzfQkrgDguI4j0Z+0/tDH/r8gtOtLLrIpuMwWHoe4vbVBFte1vj6Xt6WeE8lXwcCvLs/mcmvPqVK9jo10wWzAdBgNVHQ4EFgQUh6XzV6LwNazA+GTEVOdu07o5yOgwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwGQYDVR0RBBIwEIYOc3BpZmZlOi8vbG9jYWwwCgYIKoZIzj0EAwQDZwAwZAIwE4Me13qMC9i6Fkx0h26y09QZIbuRqA9puLg9AeeAAyo5tBzRl1YL0KNEp02VKSYJAjBdeJvqjJ9wW55OGj1JQwDFD7kWeEB6oMlwPbI/5hEY3azJi16I0uN1JSYTSWGSqWc=" ] }, { "use": "jwt-svid", "kty": "EC", "kid": "IRsID4VIM3T11TsK43Ny1DgCD5UNWhva", "crv": "P-256", "x": "64Mm92hnvqSdLxl6XQVu32-3rydodal1S8JgGYg5AGk", "y": "x5hVyA9Z4OgQxpvkhTXwNsZACk1jz1xyuTDr6JdH3R0" }, { "use": "jwt-svid", "kty": "EC", "kid": "qjwWkiMpkHzIxsSrAsLxSZ2WZ8AyMESx", "crv": "P-256", "x": "mLo0vBg7xWrcSOEhWpSmrcoVpZRBGoDDxwNJQugFzR4", "y": "7Kb3afZVERcOBWHOTWwTJLTwWX4913TSxeoTU9A0hYQ" }, { "use": "jwt-svid", "kty": "EC", "kid": "uNhqAaPI7NDn7IHOsa2ac1BF4O5qGxjZ", "crv": "P-256", "x": "pm3pJKQjBVx7x1h_dbNVCoHoXZuTwD5EAS2DjcUNsTY", "y": "jAe4nHFq0Jtr4ugIz500GfjjMfCfupIoWcPnwVWnpJc" }, { "use": "jwt-svid", "kty": "EC", "kid": "y3UHKFp0WqPpG7gVr3FKieiEzwH8fTMm", "crv": "P-256", "x": "SttM6EWWCPBRYDqGKIAqVbCcelCIJE9VBqj-uX4sgwE", "y": "4jVOkUVM-lA9GYNU8_GX5Us5fjNR_f9Hcaj7PGkgZDo" }, { "use": "jwt-svid", "kty": "EC", "kid": "mbrcuIaIUUapdCCmhQon4xJSicDmAVfK", "crv": "P-256", "x": "HvINid075yH0ssKsbmalal1LDceTuz9dN_RScnmLiaw", "y": "VBFCHK_RS4oKamHY3UdGkwbd03cC6fqEm5PI4V1crSg" }, { "use": "jwt-svid", "kty": "EC", "kid": "cHPeHMMEtvTeSMBc20DzPPhkF41BN2WJ", "crv": "P-256", "x": "hjhyHcq6nNph9QbcSIf3VzpkVWtfOT3HbRx4aZ3j-D8", "y": "KrdsDLurVCbnnYmYS2Gm0rjKkXOJ9x1-SVOjqdAQEBk" } ], "spiffe_refresh_hint": 60, "spiffe_sequence": 1 }golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/000077500000000000000000000000001474173014300230765ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/bundle.go000066400000000000000000000126741474173014300247100ustar00rootroot00000000000000package x509bundle import ( "crypto/x509" "io" "os" "sync" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) var x509bundleErr = errs.Class("x509bundle") // Bundle is a collection of trusted X.509 authorities for a trust domain. type Bundle struct { trustDomain spiffeid.TrustDomain mtx sync.RWMutex x509Authorities []*x509.Certificate } // New creates a new bundle. func New(trustDomain spiffeid.TrustDomain) *Bundle { return &Bundle{ trustDomain: trustDomain, } } // FromX509Authorities creates a bundle from X.509 certificates. func FromX509Authorities(trustDomain spiffeid.TrustDomain, authorities []*x509.Certificate) *Bundle { return &Bundle{ trustDomain: trustDomain, x509Authorities: x509util.CopyX509Authorities(authorities), } } // Load loads a bundle from a file on disk. The file must contain PEM-encoded // certificate blocks. func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) { fileBytes, err := os.ReadFile(path) if err != nil { return nil, x509bundleErr.New("unable to load X.509 bundle file: %w", err) } return Parse(trustDomain, fileBytes) } // Read decodes a bundle from a reader. The contents must be PEM-encoded // certificate blocks. func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) { b, err := io.ReadAll(r) if err != nil { return nil, x509bundleErr.New("unable to read X.509 bundle: %v", err) } return Parse(trustDomain, b) } // Parse parses a bundle from bytes. The data must be PEM-encoded certificate // blocks. func Parse(trustDomain spiffeid.TrustDomain, b []byte) (*Bundle, error) { bundle := New(trustDomain) if len(b) == 0 { return bundle, nil } certs, err := pemutil.ParseCertificates(b) if err != nil { return nil, x509bundleErr.New("cannot parse certificate: %v", err) } for _, cert := range certs { bundle.AddX509Authority(cert) } return bundle, nil } // ParseRaw parses a bundle from bytes. The certificate must be ASN.1 DER (concatenated // with no intermediate padding if there are more than one certificate) func ParseRaw(trustDomain spiffeid.TrustDomain, b []byte) (*Bundle, error) { bundle := New(trustDomain) if len(b) == 0 { return bundle, nil } certs, err := x509.ParseCertificates(b) if err != nil { return nil, x509bundleErr.New("cannot parse certificate: %v", err) } for _, cert := range certs { bundle.AddX509Authority(cert) } return bundle, nil } // TrustDomain returns the trust domain that the bundle belongs to. func (b *Bundle) TrustDomain() spiffeid.TrustDomain { return b.trustDomain } // X509Authorities returns the X.509 x509Authorities in the bundle. func (b *Bundle) X509Authorities() []*x509.Certificate { b.mtx.RLock() defer b.mtx.RUnlock() return x509util.CopyX509Authorities(b.x509Authorities) } // AddX509Authority adds an X.509 authority to the bundle. If the authority already // exists in the bundle, the contents of the bundle will remain unchanged. func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() for _, r := range b.x509Authorities { if r.Equal(x509Authority) { return } } b.x509Authorities = append(b.x509Authorities, x509Authority) } // RemoveX509Authority removes an X.509 authority from the bundle. func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() for i, r := range b.x509Authorities { if r.Equal(x509Authority) { // remove element from slice b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...) return } } } // HasX509Authority checks if the given X.509 authority exists in the bundle. func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool { b.mtx.RLock() defer b.mtx.RUnlock() for _, r := range b.x509Authorities { if r.Equal(x509Authority) { return true } } return false } // SetX509Authorities sets the X.509 authorities in the bundle. func (b *Bundle) SetX509Authorities(x509Authorities []*x509.Certificate) { b.mtx.Lock() defer b.mtx.Unlock() b.x509Authorities = x509util.CopyX509Authorities(x509Authorities) } // Empty returns true if the bundle has no X.509 x509Authorities. func (b *Bundle) Empty() bool { b.mtx.RLock() defer b.mtx.RUnlock() return len(b.x509Authorities) == 0 } // Marshal marshals the X.509 bundle into PEM-encoded certificate blocks. func (b *Bundle) Marshal() ([]byte, error) { b.mtx.RLock() defer b.mtx.RUnlock() return pemutil.EncodeCertificates(b.x509Authorities), nil } // Equal compares the bundle for equality against the given bundle. func (b *Bundle) Equal(other *Bundle) bool { if b == nil || other == nil { return b == other } return b.trustDomain == other.trustDomain && x509util.CertsEqual(b.x509Authorities, other.x509Authorities) } // Clone clones the bundle. func (b *Bundle) Clone() *Bundle { b.mtx.RLock() defer b.mtx.RUnlock() return FromX509Authorities(b.trustDomain, b.x509Authorities) } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the Source interface. An error will be // returned if the trust domain does not match that of the bundle. func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { if b.trustDomain != trustDomain { return nil, x509bundleErr.New("no X.509 bundle found for trust domain: %q", trustDomain) } return b, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/bundle_test.go000066400000000000000000000220001474173014300257270ustar00rootroot00000000000000package x509bundle_test import ( "crypto/x509" "os" "testing" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( td = spiffeid.RequireTrustDomainFromString("domain.test") td2 = spiffeid.RequireTrustDomainFromString("domain2.test") ) func TestNew(t *testing.T) { bundle := x509bundle.New(td) require.NotNil(t, bundle) assert.Len(t, bundle.X509Authorities(), 0) assert.Equal(t, td, bundle.TrustDomain()) } func TestFromX509Authorities(t *testing.T) { x509Cert1 := &x509.Certificate{ Raw: []byte("CERT 1"), } x509Cert2 := &x509.Certificate{ Raw: []byte("CERT 2"), } x509Authorities := []*x509.Certificate{x509Cert1, x509Cert2} b := x509bundle.FromX509Authorities(td, x509Authorities) require.NotNil(t, b) assert.Equal(t, b.X509Authorities(), x509Authorities) } func TestLoad_Succeeds(t *testing.T) { bundle, err := x509bundle.Load(td, "testdata/certs.pem") require.NoError(t, err) require.NotNil(t, bundle) assert.Len(t, bundle.X509Authorities(), 2) } func TestLoad_Fails(t *testing.T) { bundle, err := x509bundle.Load(td, "testdata/non-existent-file.pem") require.Error(t, err) require.Contains(t, err.Error(), "x509bundle: unable to load X.509 bundle file") assert.Nil(t, bundle) } func TestRead_Succeeds(t *testing.T) { file, err := os.Open("testdata/certs.pem") require.NoError(t, err) defer file.Close() bundle, err := x509bundle.Read(td, file) require.NoError(t, err) require.NotNil(t, bundle) assert.Len(t, bundle.X509Authorities(), 2) } func TestRead_Fails(t *testing.T) { file, err := os.Open("testdata/certs.pem") require.NoError(t, err) // Close file prematurely to cause an error while reading file.Close() bundle, err := x509bundle.Read(td, file) require.Error(t, err) require.Contains(t, err.Error(), "x509bundle: unable to read") assert.Nil(t, bundle) } func TestParse(t *testing.T) { tests := []struct { name string trustDomain spiffeid.TrustDomain path string expNumAuthorities int expErrContains string }{ { name: "Parse multiple certificates should succeed", path: "testdata/certs.pem", expNumAuthorities: 2, }, { name: "Parse single certificate should succeed", path: "testdata/cert.pem", expNumAuthorities: 1, }, { name: "Parse empty bytes should result in empty bundle", path: "testdata/empty.pem", expNumAuthorities: 0, }, { name: "Parse non-PEM bytes should fail", path: "testdata/not-pem.pem", expErrContains: "x509bundle: cannot parse certificate: no PEM blocks found", }, { name: "Parse a corrupted certificate should fail", path: "testdata/corrupted.pem", expErrContains: "x509bundle: cannot parse certificate", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { fileBytes, err := os.ReadFile(test.path) require.NoError(t, err) bundle, err := x509bundle.Parse(td, fileBytes) if test.expErrContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) assert.NotNil(t, bundle) assert.Len(t, bundle.X509Authorities(), test.expNumAuthorities) }) } } func TestParseRaw(t *testing.T) { tests := []struct { name string trustDomain spiffeid.TrustDomain path string expNumAuthorities int expErrContains string }{ { name: "Parse multiple certificates should succeed", path: "testdata/certs.pem", expNumAuthorities: 2, }, { name: "Parse single certificate should succeed", path: "testdata/cert.pem", expNumAuthorities: 1, }, { name: "Parse should not fail if no certificate block is is found", path: "testdata/empty.pem", expNumAuthorities: 0, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { certsBytes := loadRawCertificates(t, test.path) bundle, err := x509bundle.ParseRaw(td, certsBytes) if test.expErrContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) assert.NotNil(t, bundle) assert.Len(t, bundle.X509Authorities(), test.expNumAuthorities) }) } } func TestX509AuthorityCRUD(t *testing.T) { // Load bundle1, which contains a single certificate bundle1, err := x509bundle.Load(td, "testdata/cert.pem") require.NoError(t, err) assert.Len(t, bundle1.X509Authorities(), 1) // Load bundle2, which contains 2 certificates // The first certificate is the same than the one used in bundle1 bundle2, err := x509bundle.Load(td2, "testdata/certs.pem") require.NoError(t, err) assert.Len(t, bundle2.X509Authorities(), 2) assert.True(t, bundle2.HasX509Authority(bundle1.X509Authorities()[0])) // Adding a new authority increases the x509Authorities slice length bundle1.AddX509Authority(bundle2.X509Authorities()[1]) assert.Len(t, bundle1.X509Authorities(), 2) assert.True(t, bundle1.HasX509Authority(bundle2.X509Authorities()[0])) assert.True(t, bundle1.HasX509Authority(bundle2.X509Authorities()[1])) // If the authority already exist, it should not be added again bundle1.AddX509Authority(bundle2.X509Authorities()[0]) bundle1.AddX509Authority(bundle2.X509Authorities()[1]) assert.Len(t, bundle1.X509Authorities(), 2) assert.True(t, bundle1.HasX509Authority(bundle2.X509Authorities()[0])) assert.True(t, bundle1.HasX509Authority(bundle2.X509Authorities()[1])) // Removing an authority, decreases the authority slice length cert := bundle1.X509Authorities()[0] bundle1.RemoveX509Authority(cert) assert.Len(t, bundle1.X509Authorities(), 1) assert.False(t, bundle1.HasX509Authority(cert)) // If the authority does not exist, it should keep its size bundle1.RemoveX509Authority(cert) assert.Len(t, bundle1.X509Authorities(), 1) assert.False(t, bundle1.HasX509Authority(cert)) } func TestMarshal(t *testing.T) { // Load a bundle to marshal bundle, err := x509bundle.Load(td, "testdata/certs.pem") require.NoError(t, err) // Marshal the bundle pemBytes, err := bundle.Marshal() require.NoError(t, err) require.NotNil(t, pemBytes) // Load original bytes for comparison expBytes, err := os.ReadFile("testdata/certs.pem") require.NoError(t, err) // Assert the marshalled bundle is equal to the one loaded assert.Equal(t, expBytes, pemBytes) } func TestGetX509BundleForTrustDomain_Succeeds(t *testing.T) { bundle, err := x509bundle.Load(td, "testdata/certs.pem") require.NoError(t, err) b, err := bundle.GetX509BundleForTrustDomain(td) require.NoError(t, err) require.NotNil(t, b) require.Equal(t, bundle, b) } func TestGetX509BundleForTrustDomain_Fails(t *testing.T) { bundle, err := x509bundle.Load(td, "testdata/certs.pem") require.NoError(t, err) b, err := bundle.GetX509BundleForTrustDomain(td2) require.Error(t, err) require.Contains(t, err.Error(), `x509bundle: no X.509 bundle found for trust domain: "domain2.test"`) require.Nil(t, b) } func TestEqual(t *testing.T) { ca1 := test.NewCA(t, td) ca2 := test.NewCA(t, td2) empty := x509bundle.New(td) empty2 := x509bundle.New(td2) x509Authorities1 := x509bundle.FromX509Authorities(td, ca1.X509Authorities()) x509Authorities2 := x509bundle.FromX509Authorities(td, ca2.X509Authorities()) for _, tt := range []struct { name string a *x509bundle.Bundle b *x509bundle.Bundle expectEqual bool }{ { name: "empty equal", a: empty, b: empty, expectEqual: true, }, { name: "different trust domains", a: empty, b: empty2, expectEqual: false, }, { name: "X509 authorities equal", a: x509Authorities1, b: x509Authorities1, expectEqual: true, }, { name: "X509 authorities empty and not empty", a: empty, b: x509Authorities1, expectEqual: false, }, { name: "X509 authorities not empty but not equal", a: x509Authorities1, b: x509Authorities2, expectEqual: false, }, } { tt := tt t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.expectEqual, tt.a.Equal(tt.b)) }) } } func TestClone(t *testing.T) { // Load a bundle to clone original, err := x509bundle.Load(td, "testdata/certs.pem") require.NoError(t, err) cloned := original.Clone() require.True(t, original.Equal(cloned)) } func loadRawCertificates(t *testing.T, path string) []byte { certsBytes, err := os.ReadFile(path) require.NoError(t, err) if len(certsBytes) == 0 { return nil } certs, err := pemutil.ParseCertificates(certsBytes) require.NoError(t, err) var rawBytes []byte for _, cert := range certs { rawBytes = append(rawBytes, cert.Raw...) } return rawBytes } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/doc.go000066400000000000000000000025251474173014300241760ustar00rootroot00000000000000// Package x509bundle provides X.509 bundle related functionality. // // A bundle represents a collection of X.509 authorities, i.e., those that // are used to authenticate SPIFFE X509-SVIDs. // // You can create a new bundle for a specific trust domain: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := x509bundle.New(td) // // Or you can load it from disk: // // td := spiffeid.RequireTrustDomain("example.org") // bundle := x509bundle.Load(td, "bundle.pem") // // The bundle can be initialized with X.509 authorities: // // td := spiffeid.RequireTrustDomain("example.org") // var x509Authorities []*x509.Certificate = ... // bundle := x509bundle.FromX509Authorities(td, x509Authorities) // // In addition, you can add X.509 authorities to the bundle: // // var x509CA *x509.Certificate = ... // bundle.AddX509Authority(x509CA) // // Bundles can be organized into a set, keyed by trust domain: // // set := x509bundle.NewSet() // set.Add(bundle) // // A Source is source of X.509 bundles for a trust domain. Both the Bundle // and Set types implement Source: // // // Initialize the source from a bundle or set // var source x509bundle.Source = bundle // // ... or ... // var source x509bundle.Source = set // // // Use the source to query for bundles by trust domain // bundle, err := source.GetX509BundleForTrustDomain(td) package x509bundle golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/set.go000066400000000000000000000046411474173014300242250ustar00rootroot00000000000000package x509bundle import ( "sort" "sync" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Set is a set of bundles, keyed by trust domain. type Set struct { mtx sync.RWMutex bundles map[spiffeid.TrustDomain]*Bundle } // NewSet creates a new set initialized with the given bundles. func NewSet(bundles ...*Bundle) *Set { bundlesMap := make(map[spiffeid.TrustDomain]*Bundle) for _, b := range bundles { if b != nil { bundlesMap[b.trustDomain] = b } } return &Set{ bundles: bundlesMap, } } // Add adds a new bundle into the set. If a bundle already exists for the // trust domain, the existing bundle is replaced. func (s *Set) Add(bundle *Bundle) { s.mtx.Lock() defer s.mtx.Unlock() if bundle != nil { s.bundles[bundle.trustDomain] = bundle } } // Remove removes the bundle for the given trust domain. func (s *Set) Remove(trustDomain spiffeid.TrustDomain) { s.mtx.Lock() defer s.mtx.Unlock() delete(s.bundles, trustDomain) } // Has returns true if there is a bundle for the given trust domain. func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool { s.mtx.RLock() defer s.mtx.RUnlock() _, ok := s.bundles[trustDomain] return ok } // Get returns a bundle for the given trust domain. If the bundle is in the set // it is returned and the boolean is true. Otherwise, the returned value is // nil and the boolean is false. func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] return bundle, ok } // Bundles returns the bundles in the set sorted by trust domain. func (s *Set) Bundles() []*Bundle { s.mtx.RLock() defer s.mtx.RUnlock() out := make([]*Bundle, 0, len(s.bundles)) for _, bundle := range s.bundles { out = append(out, bundle) } sort.Slice(out, func(a, b int) bool { return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0 }) return out } // Len returns the number of bundles in the set. func (s *Set) Len() int { s.mtx.RLock() defer s.mtx.RUnlock() return len(s.bundles) } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the Source interface. func (s *Set) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) { s.mtx.RLock() defer s.mtx.RUnlock() bundle, ok := s.bundles[trustDomain] if !ok { return nil, x509bundleErr.New("no X.509 bundle for trust domain %q", trustDomain) } return bundle, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/set_test.go000066400000000000000000000021401474173014300252540ustar00rootroot00000000000000package x509bundle_test import ( "testing" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/stretchr/testify/require" ) var ( b1 = x509bundle.New(td) b2 = x509bundle.New(td2) ) func TestNewSet(t *testing.T) { s := x509bundle.NewSet(b1) require.True(t, s.Has(td)) s = x509bundle.NewSet(b1, b2) require.True(t, s.Has(td)) require.True(t, s.Has(td2)) } func TestAdd(t *testing.T) { s := x509bundle.NewSet() require.False(t, s.Has(td)) s.Add(b1) require.True(t, s.Has(td)) } func TestRemove(t *testing.T) { s := x509bundle.NewSet(b1) require.True(t, s.Has(td)) s.Remove(td2) require.True(t, s.Has(td)) s.Remove(td) require.False(t, s.Has(td)) } func TestHas(t *testing.T) { s := x509bundle.NewSet(b1) require.False(t, s.Has(td2)) require.True(t, s.Has(td)) } func TestSetGetX509BundleForTrustDomain(t *testing.T) { s := x509bundle.NewSet(b1) _, err := s.GetX509BundleForTrustDomain(td2) require.EqualError(t, err, `x509bundle: no X.509 bundle for trust domain "domain2.test"`) b, err := s.GetX509BundleForTrustDomain(td) require.NoError(t, err) require.Equal(t, b1, b) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/source.go000066400000000000000000000005241474173014300247260ustar00rootroot00000000000000package x509bundle import ( "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Source represents a source of X.509 bundles keyed by trust domain. type Source interface { // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) } golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/000077500000000000000000000000001474173014300247075ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/cert.pem000066400000000000000000000010321474173014300263430ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBXzCB6gIJANXCDoURTF5MMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNVBAMMDFBF TVVUSUxURVNUMTAeFw0xODA3MTYyMzU5NTZaFw00NTEyMDEyMzU5NTZaMBcxFTAT BgNVBAMMDFBFTVVUSUxURVNUMTB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQDMfDxC DcBTMAjrmo+yNBuYjavI47dPGPrqIXzfAx7L6M2Bg1ZYDaO8xXgc0+7aZZRg7Fe1 Gt0EJEourKA6qN0z4gTU5KWZrPLPwPHU75F90jgThdkmHdO7j3lr2MPjsvUCAwEA ATANBgkqhkiG9w0BAQsFAANhAEsa1QiHgPwW0V4VLtRk7xyKIyCo+D0rgQA1qLmW 69aMW12GE+sxGo7INDP2bdQGB/udG5V6FnWNTP89VwakKjU4l6LoqtUtncwoGNgT U2aPnxQpNXW7pWdBVSIBhSnptw== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/certs.pem000066400000000000000000000020641474173014300265340ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBXzCB6gIJANXCDoURTF5MMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNVBAMMDFBF TVVUSUxURVNUMTAeFw0xODA3MTYyMzU5NTZaFw00NTEyMDEyMzU5NTZaMBcxFTAT BgNVBAMMDFBFTVVUSUxURVNUMTB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQDMfDxC DcBTMAjrmo+yNBuYjavI47dPGPrqIXzfAx7L6M2Bg1ZYDaO8xXgc0+7aZZRg7Fe1 Gt0EJEourKA6qN0z4gTU5KWZrPLPwPHU75F90jgThdkmHdO7j3lr2MPjsvUCAwEA ATANBgkqhkiG9w0BAQsFAANhAEsa1QiHgPwW0V4VLtRk7xyKIyCo+D0rgQA1qLmW 69aMW12GE+sxGo7INDP2bdQGB/udG5V6FnWNTP89VwakKjU4l6LoqtUtncwoGNgT U2aPnxQpNXW7pWdBVSIBhSnptw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBXzCB6gIJAMbKbzUVGQTBMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNVBAMMDFBF TVVUSUxURVNUMjAeFw0xODA3MTYyMzU5NDNaFw00NTEyMDEyMzU5NDNaMBcxFTAT BgNVBAMMDFBFTVVUSUxURVNUMjB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQCuUQFO blDXlrJF45Hn86Mb+UAjwnECaaG9Uj7oldNwEwCimhbCQsDYTRzlAFRbdm+S6Lri 0KbhKsqDz2V4n3scLnigsLU9pLGGtAF2W/pONUIEBOwsNVL8qGW1oy6A3V0CAwEA ATANBgkqhkiG9w0BAQsFAANhACjrgsP630Mgyj7LDcyV9/tIr+f3ghjyVIyedFQo MJ0if+4o9MKN/7ius4hvI+L6M9aXGyFp/JlRK4p5upqiG6J/vrG3TNPjZMD5wen8 /oMJ7lk8yNVYR9zZQgfVzUPlcA== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/corrupted.pem000066400000000000000000000011331474173014300274170ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBXzCB6gIJANXCDoURTF5MMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNVBAMMDFBF TVVUSUxURVNUMTAeFw0xODA3MTYyMzU5NTZaFw00NTEyMDEyMzU5NTZaMBcxFTAT BgNVBAMMDFBFTVVUSUxURVNUMTB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQDMfDxC DcBTMAjrmo+yNBuYjavI47dPGPrqIXzfAx7L6M2Bg1ZYDaO8xXgc0+7aZZRg7Fe1 Gt0EJEourKA6qN0z4gTU5KWZrPLPwPHU75F90jgThdkmHdO7j3lr2MPjsvUCAwEA ATANBgkqhkiG9w0BAQsFAANhAEsa1QiHgPwW0V4VLtRk7xyKIyCo+D0rgQA1qLmW 69aMW12GE+sxGo7INDP2bdQGB/udG5V6FnWNTP89VwakKjU4l6LoqtUtncwoGNgT 69aMW12GE+sxGo7INDP2bdQGB/udG5V6FnWNTP89VwakKjU4l6LoqtUtncwoGNgT U2aPnxQpNXW7pWdBVSIBhSnptw== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/empty.pem000066400000000000000000000000001474173014300265360ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/key.pem000066400000000000000000000013151474173014300262020ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIB5QIBADANBgkqhkiG9w0BAQEFAASCAc8wggHLAgEAAmEA0EdYWa7LZ/ZA2sKk JsREA2TkXDIq01JIRsVetPFRqRW5GEmkwwk0qThebp/ofZt8oLkMwp06BLaQC3tH QGkK31jh5Djd1NHA8CpU4ByObgGeY75dhwFEzI4YwXM+e0n/AgMBAAECYFD4S4qh /4WtIE1refFwP5iqMnT9M9TvmhWZSVZCsqJvRYQBrUH9ZDGdLmkHVZTvSvKKmkoZ VvXDlpmW4Eaed8xXqsLYplMrVo6WkvdtvlvfIwP69PGFmWwKgFBe2aLHsQIxAOoT dwmlr/dNNu2MjyjcvTK0lCn6vexp6k8MaXTEsTvG0kBmVDZGuSXcKzbBoAZLxQIx AOPJU65HnDpcOM+qLH3jahTnbrg4C0BO0mj1OusLcSUnA6bFP2NkZ9LyWfMerbvG 8wIxAI7Iyt8mo50+C5iCGj250OtiPdMRsdLJlPUdRCLHbLljAZPpF8t3/q66i929 5MiSZQIwE3wXQmMxw/Q7j9f4slQPsPYTDIMOw1N6wCup/I0gApORxmQ9Bd2C3BKL CzbmmZdtAjEA2v1fSN4DPcQW2bgmoE0GoNEMYGfSza7jBGOiKkqm4p2hAjaur174 U2t9BPJHk+Xh -----END PRIVATE KEY----- golang-github-spiffe-go-spiffe-2.4.0/v2/bundle/x509bundle/testdata/not-pem.pem000066400000000000000000000000241474173014300267650ustar00rootroot00000000000000Not PEM encoded filegolang-github-spiffe-go-spiffe-2.4.0/v2/examples/000077500000000000000000000000001474173014300215445ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/README.md000066400000000000000000000024521474173014300230260ustar00rootroot00000000000000# go-spiffe (v2) Examples This section contains a set of standalone examples that demonstrate different use cases for the go-spiffe library. ## Use cases - [Mutually Authenticated TLS (mTLS)](spiffe-tls/README.md): _Establish mTLS connections between workloads using automatically rotated X.509 SVIDs obtained from the SPIFFE Workload API._ - [SVIDs stream](spiffe-watcher/README.md): _Get automatically rotated X.509 SVIDs and JWT Bundles for your workload._ - [gRPC over mTLS](spiffe-grpc/README.md): _Send gRPC requests between workloads over mTLS using automatically rotated X.509 SVIDs obtained from the SPIFFE Workload API._ - [HTTP over mTLS](spiffe-http/README.md): _Send HTTP requests between workloads over mTLS using automatically rotated X.509 SVIDs obtained from the SPIFFE Workload API._ - [HTTP over TLS with JWT and X.509 SVIDs](spiffe-jwt/README.md): _Send HTTP requests between workload over a TLS + JWT authentication using automatically rotated X.509 SVIDs and JWT SVIDs from the SPIFFE Workload API._ - [HTTP over TLS with JWT SVIDs only](spiffe-jwt-using-proxy/README.md): _Authenticate client workloads to the server using JWT-SVIDs sent over TLS-encrypted HTTP connections to handle environments in which a proxy or load balancer would prevent the transmission of X.509-SVIDs over mTLS._ golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/000077500000000000000000000000001474173014300237515ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/README.md000066400000000000000000000076331474173014300252410ustar00rootroot00000000000000# gRPC over mTLS example This example shows how two services using gRPC can communicate using mTLS with X509 SVIDs obtained from SPIFFE Workload API. Each service is connecting to the Workload API to fetch its identities. Since this example assumes the SPIRE implementation, it uses the SPIRE default socket path: `/tmp/agent.sock`. ```go source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) ``` When the socket path is not provided, the value from the `SPIFFE_ENDPOINT_SOCKET` environment variable is used. ```go source, err := workloadapi.NewX509Source(ctx) ``` The **gRPC server** uses the [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for mTLS that authenticates the client certificate and verifies that it has the SPIFFE ID `spiffe://examples.org/client`. The `tls.Config` is used to create TLS transport credentials for the gRPC server. ```go clientID := spiffeid.RequireFromString("spiffe://example.org/client") tlsConfig := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(clientID)) s := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) ``` On the other side, the **gRPC client** uses the [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for mTLS that authenticates the server certificate and verifies that it has the SPIFFE ID `spiffe://examples.org/server`. ```go serverID := spiffeid.RequireFromString("spiffe://example.org/server") tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(serverID)) conn, err := grpc.NewClient("dns:///localhost:50051", grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) ``` The [tlsconfig.Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) is used to authorize the mTLS peer. In this example, both the client and server use it to authorize the specific SPIFFE ID of the other side of the connection. That is it! The go-spiffe library fetches and automatically renews the X.509 SVIDs of both workloads from the Workload API provider (i.e. SPIRE). As soon as the mTLS connection is established, the client sends a message to the server and gets a response. ## Building Build the client workload: ```bash cd examples/spiffe-grpc/client go build ``` Build the server workload: ```bash cd examples/spiffe-grpc/server go build ``` ## Running This example assumes the following preconditions: - There are a SPIRE server and agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org` - The agent SPIFFE ID is `spiffe://example.org/host`. - There are a `server-workload` and `client-workload` users in the system. ### 1. Create the registration entries Create the registration entries for the client and server workloads: Server: ```bash ./spire-server entry create -spiffeID spiffe://example.org/server \ -parentID spiffe://example.org/host \ -selector unix:user:server-workload ``` Client: ```bash ./spire-server entry create -spiffeID spiffe://example.org/client \ -parentID spiffe://example.org/host \ -selector unix:user:client-workload ``` ### 2. Start the server Start the server with the `server-workload` user: ```bash sudo -u server-workload ./server ``` ### 3. Run the client Run the client with the `client-workload` user: ```bash sudo -u client-workload ./client ``` The server should have received a _"world"_ message and responded with a _"Hello world"_ message. If either workload encounters a peer with a different SPIFFE ID, they will abort the TLS handshake and the connection will fail. ``` sudo -u server-workload ./client Error connecting to server rpc error: code = Unavailable desc = connection closed ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/client/000077500000000000000000000000001474173014300252275ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/client/main.go000066400000000000000000000032321474173014300265020ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "github.com/spiffe/go-spiffe/v2/spiffegrpc/grpccredentials" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) // Workload API socket path const socketPath = "unix:///tmp/agent.sock" func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Create a `workloadapi.X509Source`, it will connect to Workload API using provided socket path // If socket path is not defined using `workloadapi.SourceOption`, value from environment variable `SPIFFE_ENDPOINT_SOCKET` is used. source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer source.Close() // Allowed SPIFFE ID serverID := spiffeid.RequireFromString("spiffe://example.org/server") // Dial the server with credentials that do mTLS and verify that presented certificate has SPIFFE ID `spiffe://example.org/server` conn, err := grpc.NewClient("dns:///localhost:50051", grpc.WithTransportCredentials( grpccredentials.MTLSClientCredentials(source, source, tlsconfig.AuthorizeID(serverID)), )) if err != nil { return fmt.Errorf("failed to dial: %w", err) } client := pb.NewGreeterClient(conn) reply, err := client.SayHello(ctx, &pb.HelloRequest{Name: "world"}) if err != nil { return fmt.Errorf("failed issuing RPC to server: %w", err) } log.Print(reply.Message) return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/server/000077500000000000000000000000001474173014300252575ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-grpc/server/main.go000066400000000000000000000036451474173014300265420ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "net" "github.com/spiffe/go-spiffe/v2/spiffegrpc/grpccredentials" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) const socketPath = "unix:///tmp/agent.sock" // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Create a `workloadapi.X509Source`, it will connect to Workload API using provided socket path // If socket path is not defined using `workloadapi.SourceOption`, value from environment variable `SPIFFE_ENDPOINT_SOCKET` is used. source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer source.Close() // Allowed SPIFFE ID clientID := spiffeid.RequireFromString("spiffe://example.org/client") // Create a server with credentials that do mTLS and verify that the presented certificate has SPIFFE ID `spiffe://example.org/client` s := grpc.NewServer(grpc.Creds( grpccredentials.MTLSServerCredentials(source, source, tlsconfig.AuthorizeID(clientID)), )) lis, err := net.Listen("tcp", "127.0.0.1:50051") if err != nil { return fmt.Errorf("error creating listener: %w", err) } pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { return fmt.Errorf("failed to serve: %w", err) } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/000077500000000000000000000000001474173014300237755ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/README.md000066400000000000000000000077661474173014300252740ustar00rootroot00000000000000# HTTP over mTLS This example shows how two services using HTTP can communicate using mTLS with X509 SVIDs obtained from SPIFFE workload API. Each service is connecting to the Workload API to fetch its identities. Since this example assumes the SPIRE implementation, it uses the SPIRE default socket path: `/tmp/agent.sock`. ```go source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) ``` When the socket path is not provided, the value from the `SPIFFE_ENDPOINT_SOCKET` environment variable is used. ```go source, err := workloadapi.NewX509Source(ctx) ``` The **HTTP server** uses the [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for mTLS that authenticates the client certificate and verifies that it has the SPIFFE ID `spiffe://examples.org/client`. The `tls.Config` is used when creating the HTTP server. ```go clientID := spiffeid.RequireFromString("spiffe://example.org/client") tlsConfig := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(clientID)) server := &http.Server{ Addr: ":8443", TLSConfig: tlsConfig, } ``` On the other side, the **HTTP client** uses the [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for mTLS that authenticates the server certificate and verifies that it has the SPIFFE ID `spiffe://examples.org/server`. ```go serverID := spiffeid.RequireFromString("spiffe://example.org/server") tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(serverID)) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } ``` The [tlsconfig.Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) is used to authorize the mTLS peer. In this example, both the client and server use it to authorize the specific SPIFFE ID of the other side of the connection. That is it! The go-spiffe library fetches and automatically renews the X.509 SVIDs of both workloads from the Workload API provider (i.e. SPIRE). As soon as the mTLS connection is established, the client sends an HTTP request to the server and gets a response. ## Building Build the client workload: ```bash cd examples/spiffe-http/client go build ``` Build the server workload: ```bash cd examples/spiffe-http/server go build ``` ## Running This example assumes the following preconditions: - There is a SPIRE server and agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org` - The agent SPIFFE ID is `spiffe://example.org/host`. - There is a `server-workload` and `client-workload` user in the system. ### 1. Create the registration entries Create the registration entries for the client and server workloads: Server: ```bash ./spire-server entry create -spiffeID spiffe://example.org/server \ -parentID spiffe://example.org/host \ -selector unix:user:server-workload ``` Client: ```bash ./spire-server entry create -spiffeID spiffe://example.org/client \ -parentID spiffe://example.org/host \ -selector unix:user:client-workload ``` ### 2. Start the server Start the server with the `server-workload` user: ```bash sudo -u server-workload ./server ``` ### 3. Run the client Run the client with the `client-workload` user: ```bash sudo -u client-workload ./client ``` The server should display a log `Request received` and client `Success!!!` If either workload encounters a peer with a different SPIFFE ID, they will abort the TLS handshake and the connection will fail. ``` sudo -u server-workload ./client Error connecting to "https://localhost:8443/": Get "https://localhost:8443/": remote error: tls: bad certificate ``` And server log shows ``` TLS handshake error from 127.0.0.1:52540: unexpected ID "spiffe://example.org/server" ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/client/000077500000000000000000000000001474173014300252535ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/client/main.go000066400000000000000000000031761474173014300265350ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const ( // Workload API socket path socketPath = "unix:///tmp/agent.sock" serverURL = "https://localhost:8443/" ) func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() // Create a `workloadapi.X509Source`, it will connect to Workload API using provided socket path // If socket path is not defined using `workloadapi.SourceOption`, value from environment variable `SPIFFE_ENDPOINT_SOCKET` is used. source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer source.Close() // Allowed SPIFFE ID serverID := spiffeid.RequireFromString("spiffe://example.org/server") // Create a `tls.Config` to allow mTLS connections, and verify that presented certificate has SPIFFE ID `spiffe://example.org/server` tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(serverID)) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } r, err := client.Get(serverURL) if err != nil { return fmt.Errorf("error connecting to %q: %w", serverURL, err) } defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { return fmt.Errorf("unable to read body: %w", err) } log.Printf("%s", body) return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/server/000077500000000000000000000000001474173014300253035ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-http/server/main.go000066400000000000000000000032121474173014300265540ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) // Worload API socket path const socketPath = "unix:///tmp/agent.sock" func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() // Set up a `/` resource handler http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Println("Request received") _, _ = io.WriteString(w, "Success!!!") }) // Create a `workloadapi.X509Source`, it will connect to Workload API using provided socket. // If socket path is not defined using `workloadapi.SourceOption`, value from environment variable `SPIFFE_ENDPOINT_SOCKET` is used. source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer source.Close() // Allowed SPIFFE ID clientID := spiffeid.RequireFromString("spiffe://example.org/client") // Create a `tls.Config` to allow mTLS connections, and verify that presented certificate has SPIFFE ID `spiffe://example.org/client` tlsConfig := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(clientID)) server := &http.Server{ Addr: ":8443", TLSConfig: tlsConfig, ReadHeaderTimeout: time.Second * 10, } if err := server.ListenAndServeTLS("", ""); err != nil { return fmt.Errorf("failed to serve: %w", err) } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/000077500000000000000000000000001474173014300261245ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/README.md000066400000000000000000000266631474173014300274200ustar00rootroot00000000000000# Authenticating Workloads over TLS-encrypted HTTP Connections Using JWT-SVIDs This example shows how to use the go-spiffe library to make a server workload authenticate a client workload using JWT-SVIDs fetched from the Workload API. JWT-SVIDs are useful when the workloads are not able to establish an mTLS communication channel between each other. For instance, when the server workload is behind a TLS terminating load balancer or proxy, a client workload cannot be authenticated directly by the server via mTLS and X.509-SVID. So, an alternative is to forego authenticating the client at the load balancer or proxy and instead require that clients authenticate via SPIFFE JWT-SVIDs conveyed directly to the server via the application layer. The scenario used in this example goes like this: 1. The server: - Creates an X509Source struct. - Creates a JWTSource struct. - Starts listening for HTTP requests over TLS. Only one resource is served at `/`. 2. The reverse proxy: - Creates an X509Source struct. - Starts listening for HTTP requests over TLS. It forwards requests to `/` only. 3. The client: - Creates an X509Source struct. - Creates a JWTSource struct. - Fetches a JWT-SVID using the JWTSource. - Creates a `GET /` request with the JWT-SVID set as the value of the `Authorization` header. - Sends the request to the proxy using TLS authentication for establishing the connection. 4. The proxy receives the request, logs the request's method and URL, and forwards the request to the server. 5. The server receives the request, extracts the JWT-SVID from the `Authorization` header, and verifies the token. If the token is valid, it logs `Request received` and returns a response with a body containing the string `Success!!!`, otherwise an `Unauthorized` HTTP code is returned. 6. The proxy receives the response from the server and passes it to the client. 7. The client receives the response. If the response has an HTTP 200 status, its body is logged, otherwise the HTTP status code is logged. ## Creating an X509Source struct As you may noted, the three workloads create a [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) struct. ```go x509Source, err := workloadapi.NewX509Source( ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), ) ``` Where: - ctx is a `context.Context`. `NewX509Source` function blocks until the first Workload API response is received or this context times out or is cancelled. - socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the listener would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this: ```go x509Source, err := workloadapi.NewX509Source(ctx) ``` In all cases, the `X509Source` is used to create a `tls.Config` for the underlying transport connection of the HTTP client/server. However, there are some differences in its usage on the server, client, and proxy workloads: The **server workload** uses the `X509Source` to create the [TLSServerConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSServerConfig) for the HTTP server used: ```go server := &http.Server{ Addr: ":8080", TLSConfig: tlsconfig.TLSServerConfig(x509Source), } ``` This enables the server to present an X.509-SVID to the other end of the connection. This SVID is provided by the `X509Source` via the Workload API. The **client workload** uses the `X509Source` to create the [TLSClientConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSClientConfig) for the `Transport` of the HTTP client used: ```go serverID := spiffeid.RequireFromString("spiffe://example.org/server") . . . client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsconfig.TLSClientConfig( x509Source, tlsconfig.AuthorizeID(serverID), ), }, } ``` This enables the client to verify that the X.509-SVID presented by the other end of the connection has the specified SPIFFE ID by using: - The trust bundle provided by the Workload API via the `X509Source`. - The [Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) returned by [tlsconfig.AuthorizeID()](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID) The **proxy workload** uses the `X509Source` to create the [TLSClientConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSClientConfig) for the `Transport` of the HTTP reverse proxy used: ```go proxy := httputil.NewSingleHostReverseProxy(remote) transport := *(http.DefaultTransport.(*http.Transport)) // copy of http.DefaultTransport. transport.TLSClientConfig = tlsconfig.TLSClientConfig( x509Source, tlsconfig.AuthorizeID(spiffeid.RequireFromString("spiffe://example.org/server")), ) proxy.Transport = &transport ``` This enables the proxy to verify that the X.509-SVID presented by the server has the specified SPIFFE ID by using: - The trust bundle provided by the Workload API via the `X509Source`. - The [Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) function returned by [tlsconfig.AuthorizeID()](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID) The **proxy workload** also uses the `X509Source` to create the [TLSServerConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSServerConfig) for the HTTP server used: ```go server := &http.Server{ Addr: ":8443", TLSConfig: tlsconfig.TLSServerConfig(x509Source), } ``` This enables the proxy to present an X.509-SVID to the client. This SVID is provided by the `X509Source` via the Workload API, and contains the SPIFFE ID of the server (as explained later in **Create the registration entries** section). ## Creating a JWTSource struct On the scenario described we can see that only the client and the server workloads create a [workloadapi.JWTSource](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#JWTSource). This is because the proxy workload doesn't need to deal with JWTs since the server is the one in charge of authenticating the clients: ```go jwtSource, err := workloadapi.NewJWTSource( ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), ) ``` Where: - ctx is a `context.Context`. `NewJWTSource` function blocks until the first Workload API response is received or this context times out or is cancelled. - socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the listener would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this: ```go jwtSource, err := workloadapi.NewJWTSource(ctx) ``` Although both client and server workloads create a `JWTSource`, it is used differently in each case: The **client workload** uses the `JWTSource` to get a JWT-SVID by calling its [FetchJWTSVID](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#JWTSource.FetchJWTSVID) function: ```go svid, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{ Audience: audience, }) ``` Where: - ctx is a `context.Context`. `FetchJWTSVID` method blocks until a response is received or this context times out or is cancelled. - audience is the intended recipient of the JWT-SVID. By default it is `spiffe://example.org/server`, otherwise it is equal to the value passed as the first argument of the client's executable. Then, the client uses the JWT-SVID to set a bearer token to the request's `Authorization` header: ```go req, err := http.NewRequest("GET", serverURL, nil) . . . req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", svid.Marshal())) ``` The **server workload** uses the `JWTSource` to authenticate the client by calling the [ParseAndValidate](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/svid/jwtsvid?tab=doc#ParseAndValidate) function: ```go _, err := jwtsvid.ParseAndValidate(token, a.jwtSource, a.audiences) ``` Where: - token is the marshalled JWT-SVID sent by the client in the `Authorization` header. - a.jwtSource is the `JWTSource`. - a.audiences is a slice of strings. Specifies a list of expected audiences in the `aud` field of the token. When `ParseAndValidate` returns an error, the server returns an `Unauthorized` status. Otherwise, the request continues normal processing. ## That is it! As we can see the go-spiffe library allows your application avoiding to deal with the implementation details of the Workload API. You just create the SVID sources and then simply ask the library for what you need. ## Building To build the client workload: ```bash cd examples/spiffe-jwt-using-proxy/client go build ``` To build the proxy workload: ```bash cd examples/spiffe-jwt-using-proxy/proxy go build ``` To build the server workload: ```bash cd examples/spiffe-jwt-using-proxy/server go build ``` ## Running This example assumes the following preconditions: - There is a SPIRE Server and Agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org`. - The agent SPIFFE ID is `spiffe://example.org/host`. - There are `server-workload` and `client-workload` users in the system. ### 1. Create the registration entries Create two registration entries, one for the client workload and another for the server and proxy workloads: Server: ```bash ./spire-server entry create -spiffeID spiffe://example.org/server \ -parentID spiffe://example.org/host \ -selector unix:user:server-workload ``` Client: ```bash ./spire-server entry create -spiffeID spiffe://example.org/client \ -parentID spiffe://example.org/host \ -selector unix:user:client-workload ``` We will use the `server-workload` user to run the proxy because we want it to be as transparent as possible to the client. By using this user, the proxy will get an SVID with the same SPIFFE ID as the server, due to the `unix:user:server-workload` selector used when registering the entry. ### 2. Start the server Start the server with the `server-workload` user: ```bash sudo -u server-workload ./server ``` ### 3. Start the proxy Start the proxy with the `server-workload` user: ```bash sudo -u server-workload ./proxy ``` ### 4. Run the client Run the client with the `client-workload` user: ```bash sudo -u client-workload ./client ``` For each component the logs would contain: | Component| Log content (stdout) | |----------|----------------------| | Proxy | `GET /` | | Server | `Request received` | | Client | `Success!!!` | To demonstrate a failure, we can run the client using a wrong audience as the first argument: ``` sudo -u client-workload ./client spiffe://example.org/some-other-server 401 Unauthorized ``` Given that the server expects its own SPIFFE ID as the audience value it will reject the token because of the audience's mismatch. Then server log would contain: ``` Invalid token: jwtsvid: expected audience in ["spiffe://example.org/server"] (audience=["spiffe://example.org/some-other-server"]) ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/client/000077500000000000000000000000001474173014300274025ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/client/main.go000066400000000000000000000052561474173014300306650ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "os" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const ( serverURL = "https://localhost:8443" socketPath = "unix:///tmp/agent.sock" ) func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Set a timeout to prevent the request from hanging if this workload is not properly registered in SPIRE. ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() clientOptions := workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)) // Create an X509Source struct to fetch the trust bundle as needed to verify the X509-SVID presented by the server. x509Source, err := workloadapi.NewX509Source(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer x509Source.Close() serverID := spiffeid.RequireFromString("spiffe://example.org/server") // By default, this example uses the server's SPIFFE ID as the audience. // It doesn't have to be a SPIFFE ID as long as it follows the JWT-SVID guidelines (https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#32-audience) audience := serverID.String() args := os.Args if len(args) >= 2 { audience = args[1] } // Create a JWTSource to fetch JWT-SVIDs jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create JWTSource: %w", err) } defer jwtSource.Close() // Fetch a JWT-SVID and set the `Authorization` header. // Alternatively, it is possible to fetch the JWT-SVID using `workloadapi.FetchJWTSVID`. svid, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{ Audience: audience, }) if err != nil { return fmt.Errorf("unable to fetch SVID: %w", err) } req, err := http.NewRequest("GET", serverURL, nil) if err != nil { return fmt.Errorf("unable to create request: %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", svid.Marshal())) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsconfig.TLSClientConfig( x509Source, tlsconfig.AuthorizeID(serverID), ), }, } res, err := client.Do(req) if err != nil { return fmt.Errorf("unable to connect to %q: %w", serverURL, err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status: %d", res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("error reading response body: %w", err) } log.Printf("%s", body) return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/proxy/000077500000000000000000000000001474173014300273055ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/proxy/main.go000066400000000000000000000044121474173014300305610ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "net/http" "net/http/httputil" "net/url" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const socketPath = "unix:///tmp/agent.sock" func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { remote, err := url.Parse("https://localhost:8080") if err != nil { return fmt.Errorf("unable to parse server URL: %w", err) } // Set a timeout to prevent the request from hanging if this workload is not properly registered in SPIRE. ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Create an X509Source struct to fetch a SPIFFE X.509-SVID automatically from the // Workload API, and use it to establish the TLS connection by presenting it // to the client. x509Source, err := workloadapi.NewX509Source( ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), ) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer x509Source.Close() // Create a ReverseProxy using a TLSClient config that can connect only // to a server that presents an X.509-SVID having "spiffe://example.org/server" // as its SPIFFE ID. proxy := httputil.NewSingleHostReverseProxy(remote) transport := *(http.DefaultTransport.(*http.Transport)) //nolint transport.TLSClientConfig = tlsconfig.TLSClientConfig( x509Source, tlsconfig.AuthorizeID(spiffeid.RequireFromString("spiffe://example.org/server")), ) proxy.Transport = &transport http.HandleFunc("/", handler(proxy)) // Create an HTTP server using a TLS configuration that doesn't require // client certificates, because the proxy is not in charge of authenticating // the clients. server := &http.Server{ Addr: ":8443", TLSConfig: tlsconfig.TLSServerConfig(x509Source), ReadHeaderTimeout: time.Second * 10, } if err := server.ListenAndServeTLS("", ""); err != nil { return fmt.Errorf("failed to serve: %w", err) } return nil } func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL) p.ServeHTTP(w, r) } } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/server/000077500000000000000000000000001474173014300274325ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt-using-proxy/server/main.go000066400000000000000000000051471474173014300307140ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "strings" "time" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const socketPath = "unix:///tmp/agent.sock" func index(w http.ResponseWriter, r *http.Request) { log.Println("Request received") _, _ = io.WriteString(w, "Success!!!") } type authenticator struct { // JWTSource used to verify the received token jwtSource *workloadapi.JWTSource // Expected audiences audiences []string } func (a *authenticator) authenticateClient(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { fields := strings.Fields(req.Header.Get("Authorization")) if len(fields) != 2 || fields[0] != "Bearer" { log.Print("Malformed header") http.Error(w, "Invalid or unsupported authorization header", http.StatusUnauthorized) return } token := fields[1] _, err := jwtsvid.ParseAndValidate(token, a.jwtSource, a.audiences) if err != nil { log.Printf("Invalid token: %v\n", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, req) }) } func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Create options to configure Sources to use SPIRE Agent's expected socket path. // By default, Sources uses the value of the `SPIFFE_ENDPOINT_SOCKET` environment variable, // so creating this is not required. clientOptions := workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)) // Create an X509Source for the server's TLS configuration. x509Source, err := workloadapi.NewX509Source( ctx, clientOptions, ) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer x509Source.Close() // Create a JWTSource to validate tokens provided by clients. jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create JWTSource: %w", err) } defer jwtSource.Close() // Add a handler to act as a middleware to validate the JWT-SVID presented by the client. auth := &authenticator{ jwtSource: jwtSource, audiences: []string{"spiffe://example.org/server"}, } http.Handle("/", auth.authenticateClient(http.HandlerFunc(index))) server := &http.Server{ Addr: ":8080", TLSConfig: tlsconfig.TLSServerConfig(x509Source), ReadHeaderTimeout: time.Second * 10, } if err := server.ListenAndServeTLS("", ""); err != nil { return fmt.Errorf("failed to serve: %w", err) } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/000077500000000000000000000000001474173014300236225ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/README.md000066400000000000000000000115401474173014300251020ustar00rootroot00000000000000# HTTP over TLS with JWT This example shows how two services using HTTP can communicate using TLS with the server presenting an X509 SVID and expecting a client to authenticate with a JWT-SVID. The SVIDs are retrieved, and authentication is accomplished, via the SPIFFE Workload API. The **HTTP server** creates a [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source). ```go source, err := workloadapi.NewX509Source(ctx, clientOptions) ``` The socket path is provided as a client option. If the socket path is not provided, the value from the `SPIFFE_ENDPOINT_SOCKET` environment variable is used. ```go source, err := workloadapi.NewX509Source(ctx) ``` The **HTTP server** then uses [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for TLS that will present the server X509-SVID. The `tls.Config` is used when creating the HTTP server. ```go tlsConfig := tlsconfig.TLSServerConfig(source) server := &http.Server{ Addr: ":8443", TLSConfig: tlsConfig, } ``` The server creates a JWTSource to obtain up-to-date JWT bundles from the Workload API. ```go jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) ``` A middleware is added to authenticate client JWT-SVIDs provided in the `Authorization` header. This middleware validates the token using the [jwtsvid.ParseAndValidate](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/svid/jwtsvid?tab=doc#ParseAndValidate) using bundles obtained from the JWTSource. ```go svid, err := jwtsvid.ParseAndValidate(token, a.jwtSource, a.audiences) ``` As an alternative to verifying the JWT-SVIDs directly, the Workload API can also be used: ```go client, err := workloadapi.New(ctx) if err != nil { log.Fatalf("Unable to connect to Workload API: %v", err) } svid, err := client.ValidateJWTSVID(ctx, token, audiences[0]) ``` On the other side, the **HTTP client** uses the [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) to create a `tls.Config` for TLS that authenticates the server certificate and verifies that it has the SPIFFE ID `spiffe://examples.org/server`. ```go serverID := spiffeid.RequireFromString("spiffe://example.org/server") tlsConfig := tlsconfig.TLSClientConfig(source, tlsconfig.AuthorizeID(serverID)) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } ``` The client fetches a JWT-SVID from the Workload API (via the JWTSource) and adds it as a bearer token in the `Authorization` header. ```go svid, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{ Audience: audience, }) if err != nil { log.Fatalf("Unable to fetch SVID: %v", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", svid.Marshal())) ``` That is it! The go-spiffe library fetches and automatically renews the X.509 SVID for the server and validates the client JWT SVIDs using the Workload API. As soon as the TLS connection is established, the client sends an HTTP request to the server and gets a response. ## Building Build the client workload: ```bash cd examples/spiffe-jwt/client go build ``` Build the server workload: ```bash cd examples/spiffe-jwt/server go build ``` ## Running This example assumes the following preconditions: - There is a SPIRE Server and Agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org` - The agent SPIFFE ID is `spiffe://example.org/host`. - There is a `server-workload` and `client-workload` user in the system. ### 1. Create the registration entries Create the registration entries for the client and server workloads: Server: ```bash ./spire-server entry create -spiffeID spiffe://example.org/server \ -parentID spiffe://example.org/host \ -selector unix:user:server-workload ``` Client: ```bash ./spire-server entry create -spiffeID spiffe://example.org/client \ -parentID spiffe://example.org/host \ -selector unix:user:client-workload ``` ### 2. Start the server Start the server with the `server-workload` user: ```bash sudo -u server-workload ./server ``` ### 3. Run the client Run the client with the `client-workload` user: ```bash sudo -u client-workload ./client ``` The server should display a log `Request received` and client `Success!!!` To demonstrate a failure, an alternate audience value can be used. The server is expecting its own SPIFFE ID as the audience value and will reject the token if it doesn't match. ``` sudo -u client-workload ./client spiffe://example.org/some-other-server Unauthorized ``` When the token is rejected, the server log shows: ``` Invalid token: jwtsvid: expected audience in ["spiffe://example.org/server"] (audience=["spiffe://example.org/some-other-server"]) ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/client/000077500000000000000000000000001474173014300251005ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/client/main.go000066400000000000000000000055501474173014300263600ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "os" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const ( serverURL = "https://localhost:8443" socketPath = "unix:///tmp/agent.sock" ) func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Time out the example after 30 seconds. This prevents the example from hanging if the workloads are not properly registered with SPIRE. ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Create client options to setup expected socket path, // as default sources will use value from environment variable `SPIFFE_ENDPOINT_SOCKET` clientOptions := workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)) // Create X509 source to fetch bundle certificate used to verify presented certificate from server x509Source, err := workloadapi.NewX509Source(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer x509Source.Close() // Create a `tls.Config` with configuration to allow TLS communication, and verify that presented certificate from server has SPIFFE ID `spiffe://example.org/server` serverID := spiffeid.RequireFromString("spiffe://example.org/server") tlsConfig := tlsconfig.TLSClientConfig(x509Source, tlsconfig.AuthorizeID(serverID)) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } req, err := http.NewRequest("GET", serverURL, nil) if err != nil { return fmt.Errorf("unable to create request: %w", err) } // As default example is using server's ID, // It doesn't have to be an SPIFFE ID as long it follows JWT SVIDs the guidelines (https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md#32-audience) audience := serverID.String() args := os.Args if len(args) >= 2 { audience = args[1] } // Create a JWTSource to fetch SVIDs jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create JWTSource: %w", err) } defer jwtSource.Close() // Fetch JWT SVID and add it to `Authorization` header, // It is possible to fetch JWT SVID using `workloadapi.FetchJWTSVID` svid, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{ Audience: audience, }) if err != nil { return fmt.Errorf("unable to fetch SVID: %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", svid.Marshal())) res, err := client.Do(req) if err != nil { return fmt.Errorf("unable to issue request to %q: %w", serverURL, err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("error reading response body: %w", err) } log.Printf("%s", body) return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/server/000077500000000000000000000000001474173014300251305ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-jwt/server/main.go000066400000000000000000000063131474173014300264060ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "net/http" "strings" "time" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const socketPath = "unix:///tmp/agent.sock" func index(w http.ResponseWriter, r *http.Request) { log.Println("Request received", svidClaims(r.Context())) _, _ = io.WriteString(w, "Success!!!") } type authenticator struct { // JWT Source used to verify token jwtSource *workloadapi.JWTSource // Expected audiences audiences []string } func (a *authenticator) authenticateClient(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { fields := strings.Fields(req.Header.Get("Authorization")) if len(fields) != 2 || fields[0] != "Bearer" { log.Print("Malformed header") http.Error(w, "Invalid or unsupported authorization header", http.StatusUnauthorized) return } token := fields[1] // Parse and validate token against fetched bundle from jwtSource, // an alternative is using `workloadapi.ValidateJWTSVID` that will // attest against SPIRE on each call and validate token svid, err := jwtsvid.ParseAndValidate(token, a.jwtSource, a.audiences) if err != nil { log.Printf("Invalid token: %v\n", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } req = req.WithContext(withSVIDClaims(req.Context(), svid.Claims)) next.ServeHTTP(w, req) }) } type svidClaimsKey struct{} func withSVIDClaims(ctx context.Context, claims map[string]interface{}) context.Context { return context.WithValue(ctx, svidClaimsKey{}, claims) } func svidClaims(ctx context.Context) map[string]interface{} { claims, _ := ctx.Value(svidClaimsKey{}).(map[string]interface{}) return claims } func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Create options to configure Sources to use expected socket path, // as default sources will use value environment variable `SPIFFE_ENDPOINT_SOCKET` clientOptions := workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)) // Create a X509Source using previously create workloadapi client x509Source, err := workloadapi.NewX509Source(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create X509Source: %w", err) } defer x509Source.Close() // Create a `tls.Config` with configuration to allow TLS communication with client tlsConfig := tlsconfig.TLSServerConfig(x509Source) server := &http.Server{ Addr: ":8443", TLSConfig: tlsConfig, ReadHeaderTimeout: time.Second * 10, } // Create a JWTSource to validate provided tokens from clients jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) if err != nil { return fmt.Errorf("unable to create JWTSource: %w", err) } defer jwtSource.Close() // Add a middleware to validate presented JWT token auth := &authenticator{ jwtSource: jwtSource, audiences: []string{"spiffe://example.org/server"}, } http.Handle("/", auth.authenticateClient(http.HandlerFunc(index))) if err := server.ListenAndServeTLS("", ""); err != nil { return fmt.Errorf("failed to serve: %w", err) } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/000077500000000000000000000000001474173014300236205ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/README.md000066400000000000000000000154431474173014300251060ustar00rootroot00000000000000# Mutually Authenticated TLS (mTLS) This example shows how to use the go-spiffe library to establish an mTLS connection between two workloads using X.509 SVIDs obtained from the SPIFFE Workload API. One workload acts as a client and the other as the server. The scenario goes like this: 1. The server starts listening for incoming SPIFFE-compliant mTLS connections. 2. The client establishes an SPIFFE-compliant mTLS connection to the server. 3. The server starts waiting for a message from the client. 4. The client sends a "Hello server" message and starts waiting for a response. 5. The server reads the client's message, logs it to stdout, and sends a "Hello client" message as the response. 6. The client reads the server's response and then closes the connection. ## Listening To start listening for incoming connections the **server workload** uses the [spiffetls.ListenWithMode](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls?tab=doc#ListenWithMode) function as follows: ```go listener, err := spiffetls.ListenWithMode(ctx, "tcp", serverAddress, spiffetls.MTLSServerWithSourceOptions( tlsconfig.AuthorizeID(clientID), workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), )) ``` Where: - ctx is a `context.Context`. `ListenWithMode` blocks until the first Workload API response is received or this context times out or is cancelled. - serverAddress is the address (`localhost:55555`) where the server workload is going to listen for client connections. - [spiffetls.MTLSServerWithSourceOptions](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls?tab=doc#MTLSServerWithSourceOptions) is used to configure the [X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2@v2.0.0-alpha.3/workloadapi?tab=doc#X509Source) used by the internal Workload API client. - clientID is a SPIFFE ID (`spiffe://example.org/client`), which along with the [tlsconfig.AuthorizeID](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID) function configures the server to accept only clients that present an X509-SVID with a matching SPIFFE ID. You can pick any of the functions that return a [tlsconfig.Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) included with the library, or you can make your own. - socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the listener would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this: ```go listener, err := spiffetls.Listen(ctx, "tcp", serverAddress, tlsconfig.AuthorizeID(spiffeID)) ``` ## Dialing To establish a connection, the **client workload** uses the [spiffetls.DialWithMode](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls?tab=doc#DialWithMode) function as follows: ```go conn, err := spiffetls.DialWithMode(ctx, "tcp", serverAddress, spiffetls.MTLSClientWithSourceOptions( tlsconfig.AuthorizeID(spiffeID), workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), )) ``` Where: - ctx is a `context.Context`. `DialWithMode` blocks until the first Workload API response is received or this context times out or is cancelled. - serverAddress is the address (`localhost:55555`) where the server workload is listening for client connections. - [spiffetls.MTLSClientWithSourceOptions](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls?tab=doc#MTLSClientWithSourceOptions) is used to configure the [X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2@v2.0.0-alpha.3/workloadapi?tab=doc#X509Source) used by the internal Workload API client. - spiffeID is a SPIFFE ID (`spiffe://example.org/server`), which along with the [tlsconfig.AuthorizeID](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID) function configures the client to connect only to a server that presents an X509-SVID with a matching SPIFFE ID. You can pick any of the functions that return a [tlsconfig.Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) included with the library, or you can make your own. - socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the dialer would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this: ```go conn, err := spiffetls.Dial(ctx, "tcp", serverAddress, tlsconfig.AuthorizeID(spiffeID)) ``` ## That is it! As we can see the go-spiffe library allows your application to use the Workload API transparently for both ends of the connection. The go-spiffe library takes care of fetching and automatically renewing the X.509 SVIDs needed to maintain a secure communication. ## Building To build the client workload: ```bash cd examples/spiffe-tls/client go build ``` To build the server workload: ```bash cd examples/spiffe-tls/server go build ``` ## Running This example assumes the following preconditions: - There is a SPIRE server and a SPIRE agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org`. - The agent's SPIFFE ID is `spiffe://example.org/host`. - There is a `server-workload` user and a `client-workload` user in the system. ### 1. Create the registration entries Create the registration entries for the workloads: Server: ```bash ./spire-server entry create -spiffeID spiffe://example.org/server \ -parentID spiffe://example.org/host \ -selector unix:user:server-workload ``` Client: ```bash ./spire-server entry create -spiffeID spiffe://example.org/client \ -parentID spiffe://example.org/host \ -selector unix:user:client-workload ``` ### 2. Start the server Start the server with the `server-workload` user: ```bash sudo -u server-workload ./server ``` ### 3. Run the client Run the client with the `client-workload` user: ```bash sudo -u client-workload ./client ``` The server should have received a _"Hello server"_ message and responded with a _"Hello client"_ message. If either workload encounters a peer with a different SPIFFE ID than the one it expects, the workload aborts the TLS handshake and the connection fails. For instance, when running the client with the server's user: ``` sudo -u server-workload ./client Unable to read server response: remote error: tls: bad certificate ``` The server log would contain: ``` Error reading incoming data: unexpected ID "spiffe://example.org/server" ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/client/000077500000000000000000000000001474173014300250765ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/client/main.go000066400000000000000000000030121474173014300263450ustar00rootroot00000000000000package main import ( "bufio" "context" "fmt" "io" "log" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const ( socketPath = "unix:///tmp/agent.sock" serverAddress = "localhost:55555" ) func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // Allowed SPIFFE ID serverID := spiffeid.RequireFromString("spiffe://example.org/server") // Create a TLS connection. // The client expects the server to present an SVID with the spiffeID: 'spiffe://example.org/server' // // An alternative when creating Dial is using `spiffetls.Dial` that uses environment variable `SPIFFE_ENDPOINT_SOCKET` conn, err := spiffetls.DialWithMode(ctx, "tcp", serverAddress, spiffetls.MTLSClientWithSourceOptions( tlsconfig.AuthorizeID(serverID), workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), )) if err != nil { return fmt.Errorf("unable to create TLS connection: %w", err) } defer conn.Close() // Send a message to the server using the TLS connection fmt.Fprintf(conn, "Hello server\n") // Read server response status, err := bufio.NewReader(conn).ReadString('\n') if err != nil && err != io.EOF { return fmt.Errorf("unable to read server response: %w", err) } log.Printf("Server says: %q", status) return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/server/000077500000000000000000000000001474173014300251265ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-tls/server/main.go000066400000000000000000000033371474173014300264070ustar00rootroot00000000000000package main import ( "bufio" "context" "fmt" "log" "net" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const ( socketPath = "unix:///tmp/agent.sock" serverAddress = "localhost:55555" ) func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { // Allowed SPIFFE ID clientID := spiffeid.RequireFromString("spiffe://example.org/client") // Creates a TLS listener // The server expects the client to present an SVID with the spiffeID: 'spiffe://example.org/client' // // An alternative when creating Listen is using `spiffetls.Listen` that uses environment variable `SPIFFE_ENDPOINT_SOCKET` listener, err := spiffetls.ListenWithMode(ctx, "tcp", serverAddress, spiffetls.MTLSServerWithSourceOptions( tlsconfig.AuthorizeID(clientID), workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)), )) if err != nil { return fmt.Errorf("unable to create TLS listener: %w", err) } defer listener.Close() // Handle connections for { conn, err := listener.Accept() if err != nil { return fmt.Errorf("failed to accept connection: %w", err) } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() // Read incoming data into buffer req, err := bufio.NewReader(conn).ReadString('\n') if err != nil { log.Printf("Error reading incoming data: %v", err) return } log.Printf("Client says: %q", req) // Send a response back to the client if _, err = conn.Write([]byte("Hello client\n")); err != nil { log.Printf("Unable to send response: %v", err) return } } golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-watcher/000077500000000000000000000000001474173014300244535ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-watcher/README.md000066400000000000000000000076041474173014300257410ustar00rootroot00000000000000# X.509 SVID Watcher example This example shows how a service can obtain automatically rotated X.509 SVIDs and JWT Bundles from the SPIFFE Workload API. The first step is to create a Workload API client. The code assumes it is talking to [SPIRE](https://github.com/spiffe/spire) and uses a [workloadapi.ClientOption](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#ClientOption) to provide the address to the SPIRE Agent socket. ```go client, err := workloadapi.New(ctx, workloadapi.WithAddr(socketPath)) ``` In case `workloadapi.WithAddr` is not provided, the value of `SPIFFE_ENDPOINT_SOCKET` environment variable will be used ```go client, err := workloadapi.New(ctx) ``` The library uses a watcher interface [workloadapi.X509ContextWatcher](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509ContextWatcher) to send updates (or errors) to clients. ```go err = client.WatchX509Context(ctx, &x509Watcher{}) ``` The watcher will be notified every time an SVID is updated or an error occurs. It is possible to watch for JWT Bundles updates: ```go err = client.WatchJWTBundles(ctx, &jwtWatcher{}) ``` The watcher will be notified every time a JWT Bundle is updated or an error occurs. ## Building Build the spiffe-watcher example: ```bash cd examples/spiffe-watcher go build ``` ## Running This example assumes the following preconditions: - There is a SPIRE server and agent up and running. - There is a Unix workload attestor configured. - The trust domain is `example.org` - The agent SPIFFE ID is `spiffe://example.org/host`. - There is a `spiffe-watcher` user in the system. ### 1. Create the registration entry Create the registration entry for the spiffe-watcher workload: ```bash ./spire-server entry create -spiffeID spiffe://example.org/spiffe-watcher \ -parentID spiffe://example.org/host \ -selector unix:user:spiffe-watcher ``` ### 2. Start the workload Start the spiffe-watcher with the `spiffe-watcher` user: ```bash sudo -u spiffe-watcher ./spiffe-watcher ``` The watcher prints the SVID SPIFFE ID every time an SVID is updated. ``` SVID updated for "spiffe://example.org/spiffe-watcher": -----BEGIN CERTIFICATE----- MIIB5TCCAYugAwIBAgIRAIkzRMvOixmAvYiJwow5AOUwCgYIKoZIzj0EAwIwHjEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMDA0MjIxNjA2MjJaFw0y MDA0MjIxNzA2MzJaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG ByqGSM49AgEGCCqGSM49AwEHA0IABISDihEjzQNw0lBVmG6N8g1dSxA5qZpd5Xyb ilpilnmmZZsCXz3LkShtk3Jem7TfTDcpAVNWEApz4whSm78ICwOjgaowgacwDgYD VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV HRMBAf8EAjAAMB0GA1UdDgQWBBSEyoGr5Y9bdpNedAwpxW9O6zBDmzAfBgNVHSME GDAWgBTLGktJw00mGe07dUGY6JQkyghF3TAoBgNVHREEITAfhh1zcGlmZmU6Ly9l eGFtcGxlLm9yZy93b3JrbG9hZDAKBggqhkjOPQQDAgNIADBFAiEAnOqqI+fKDPQn QVgh01bmWy00DNWWYpKuAQakj9zk4PMCIDsNbYwEztgxlsb1DxZlqJpR2gZkdoAJ FnFdqZr2XMrT -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIB4TCCAWegAwIBAgIRAP/k5B666y4MrrdwssWL6rUwCgYIKoZIzj0EAwMwHjEL MAkGA1UEBhMCVVMxDzANBgNVBAoMBlNQSUZGRTAeFw0yMDA0MjIxNDM3NTBaFw0y MDA0MjMxNDM4MDBaMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQKEwZTUElGRkUwWTAT BgcqhkjOPQIBBggqhkjOPQMBBwNCAARleC9KdQcH05tTgYfihasPGxeeo1kXztL4 1Th5vUF2D0In7kCcwZu3o9m9mguiJ4aeTFXL5QVU0ju6GiCdFXH/o4GFMIGCMA4G A1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTLGktJw00m Ge07dUGY6JQkyghF3TAfBgNVHSMEGDAWgBSHpfNXovA1rMD4ZMRU527TujnI6DAf BgNVHREEGDAWhhRzcGlmZmU6Ly9leGFtcGxlLm9yZzAKBggqhkjOPQQDAwNoADBl AjArmjEf1aHXvqfy9pOC+7ZKon22x1FV4tHbNWuRvPZEyy86cDCkU6uaBgjJ3GKR +gcCMQDMoTlCekCTdCfHeQOy7kbr5fjXFiw0+SnO/4iFGBLnrDcnIIpxCdzKL4HW gLI5JLc= -----END CERTIFICATE----- jwt bundle updated "example.org": {"keys":[{"kty":"EC","kid":"KULvTqUAs9SwuYGoO06ifavOQkA5Dkic","crv":"P-256","x":"WtHZ13-FO_B4SXhYbtXE-e7TmFl_txMOtY-Ls3jWPeE","y":"UNkGvC4MYOUXbgoHRCXGAtSTVE9zqXCkecjTB2cj9RA"}]} jwt bundle updated "example.org": {"keys":[{"kty":"EC","kid":"KULvTqUAs9SwuYGoO06ifavOQkA5Dkic"," ``` golang-github-spiffe-go-spiffe-2.4.0/v2/examples/spiffe-watcher/main.go000066400000000000000000000056511474173014300257350ustar00rootroot00000000000000package main import ( "context" "log" "os" "os/signal" "sync" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/workloadapi" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const socketPath = "unix:///tmp/agent.sock" func main() { ctx, cancel := context.WithCancel(context.Background()) // Wait for an os.Interrupt signal go waitForCtrlC(cancel) // Start X.509 and JWT watchers startWatchers(ctx) } func startWatchers(ctx context.Context) { var wg sync.WaitGroup // Creates a new Workload API client, connecting to provided socket path // Environment variable `SPIFFE_ENDPOINT_SOCKET` is used as default client, err := workloadapi.New(ctx, workloadapi.WithAddr(socketPath)) if err != nil { log.Fatalf("Unable to create workload API client: %v", err) } defer client.Close() wg.Add(1) // Start a watcher for X.509 SVID updates go func() { defer wg.Done() err := client.WatchX509Context(ctx, &x509Watcher{}) if err != nil && status.Code(err) != codes.Canceled { log.Fatalf("Error watching X.509 context: %v", err) } }() wg.Add(1) // Start a watcher for JWT bundle updates go func() { defer wg.Done() err := client.WatchJWTBundles(ctx, &jwtWatcher{}) if err != nil && status.Code(err) != codes.Canceled { log.Fatalf("Error watching JWT bundles: %v", err) } }() wg.Wait() } // x509Watcher is a sample implementation of the workloadapi.X509ContextWatcher interface type x509Watcher struct{} // UpdateX509SVIDs is run every time an SVID is updated func (x509Watcher) OnX509ContextUpdate(c *workloadapi.X509Context) { for _, svid := range c.SVIDs { pem, _, err := svid.Marshal() if err != nil { log.Fatalf("Unable to marshal X.509 SVID: %v", err) } log.Printf("SVID updated for %q: \n%s\n", svid.ID, string(pem)) } } // OnX509ContextWatchError is run when the client runs into an error func (x509Watcher) OnX509ContextWatchError(err error) { if status.Code(err) != codes.Canceled { log.Printf("OnX509ContextWatchError error: %v", err) } } // jwtWatcher is a sample implementation of the workloadapi.JWTBundleWatcher interface type jwtWatcher struct{} // UpdateX509SVIDs is run every time a JWT Bundle is updated func (jwtWatcher) OnJWTBundlesUpdate(bundleSet *jwtbundle.Set) { for _, bundle := range bundleSet.Bundles() { jwt, err := bundle.Marshal() if err != nil { log.Fatalf("Unable to marshal JWT Bundle : %v", err) } log.Printf("jwt bundle updated %q: %s", bundle.TrustDomain(), string(jwt)) } } // OnJWTBundlesWatchError is run when the client runs into an error func (jwtWatcher) OnJWTBundlesWatchError(err error) { if status.Code(err) != codes.Canceled { log.Printf("OnJWTBundlesWatchError error: %v", err) } } // waitForCtrlC waits until an os.Interrupt signal is sent (ctrl + c) func waitForCtrlC(cancel context.CancelFunc) { signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt) <-signalCh cancel() } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/000077500000000000000000000000001474173014300220465ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/federation/examples_test.go000066400000000000000000000107551474173014300252620ustar00rootroot00000000000000package federation_test import ( "context" "net/http" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/federation" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) func ExampleFetchBundle_webPKI() { endpointURL := "https://example.org:8443/bundle" trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } bundle, err := federation.FetchBundle(context.TODO(), trustDomain, endpointURL) if err != nil { // TODO: handle error } // TODO: use bundle bundle = bundle } func ExampleFetchBundle_sPIFFEAuth() { // Obtain a bundle from the example.org trust domain from a server hosted // at https://example.org/bundle with the // spiffe://example.org/bundle-server SPIFFE ID. endpointURL := "https://example.org:8443/bundle" trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } serverID := spiffeid.RequireFromPath(trustDomain, "/bundle-server") bundle, err := spiffebundle.Load(trustDomain, "bundle.json") if err != nil { // TODO: handle error } bundleSet := spiffebundle.NewSet(bundle) bundleSet.Add(bundle) updatedBundle, err := federation.FetchBundle(context.TODO(), trustDomain, endpointURL, federation.WithSPIFFEAuth(bundleSet, serverID)) if err != nil { // TODO: handle error } // TODO: use bundle, e.g. replace the bundle in the bundle set so it can // be used to fetch the next bundle. bundleSet.Add(updatedBundle) } func ExampleWatchBundle_webPKI() { endpointURL := "https://example.org:8443/bundle" trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } var watcher federation.BundleWatcher err = federation.WatchBundle(context.TODO(), trustDomain, endpointURL, watcher) if err != nil { // TODO: handle error } } func ExampleWatchBundle_sPIFFEAuth() { // Watch for bundle updates from the example.org trust domain from a server // hosted at https://example.org/bundle with the // spiffe://example.org/bundle-server SPIFFE ID. endpointURL := "https://example.org:8443/bundle" trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } serverID := spiffeid.RequireFromPath(trustDomain, "/bundle-server") bundle, err := spiffebundle.Load(trustDomain, "bundle.json") if err != nil { // TODO: handle error } bundleSet := spiffebundle.NewSet(bundle) bundleSet.Add(bundle) // TODO: When implementing the watcher's OnUpdate, replace the bundle for // the trust domain in the bundle set so the next connection uses the // updated bundle. var watcher federation.BundleWatcher err = federation.WatchBundle(context.TODO(), trustDomain, endpointURL, watcher, federation.WithSPIFFEAuth(bundleSet, serverID)) if err != nil { // TODO: handle error } } func ExampleHandler_webPKI() { trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } bundleSource, err := workloadapi.NewBundleSource(context.TODO()) if err != nil { // TODO: handle error } defer bundleSource.Close() handler, err := federation.NewHandler(trustDomain, bundleSource) if err != nil { // TODO: handle error } server := http.Server{ Addr: ":8443", Handler: handler, ReadHeaderTimeout: time.Second * 10, // TODO: set this appropriately } if err := server.ListenAndServeTLS("", ""); err != nil { // TODO: handle error } } func ExampleHandler_sPIFFEAuth() { trustDomain, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: handle error } // Create an X.509 source for obtaining the server X509-SVID x509Source, err := workloadapi.NewX509Source(context.TODO()) if err != nil { // TODO: handle error } defer x509Source.Close() // Create a bundle source for obtaining the bundle for the trust domain bundleSource, err := workloadapi.NewBundleSource(context.TODO()) if err != nil { // TODO: handle error } defer bundleSource.Close() handler, err := federation.NewHandler(trustDomain, bundleSource) if err != nil { // TODO: handle error } server := http.Server{ Addr: ":8443", Handler: handler, ReadHeaderTimeout: time.Second * 10, // TODO: set this appropriately TLSConfig: tlsconfig.TLSServerConfig(x509Source), } if err := server.ListenAndServeTLS("", ""); err != nil { // TODO: handle error } } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/fetch.go000066400000000000000000000056731474173014300235010ustar00rootroot00000000000000package federation import ( "context" "crypto/tls" "crypto/x509" "net/http" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/zeebo/errs" ) var federationErr = errs.Class("federation") // FetchOption is an option used when dialing the bundle endpoint. type FetchOption interface { apply(*fetchOptions) error } type fetchOptions struct { transport *http.Transport authMethod authMethod } // WithSPIFFEAuth authenticates the bundle endpoint with SPIFFE authentication // using the provided root store. It validates that the endpoint presents the // expected SPIFFE ID. This option cannot be used in conjuntion with WithWebPKIRoots // option. func WithSPIFFEAuth(bundleSource x509bundle.Source, endpointID spiffeid.ID) FetchOption { return fetchOption(func(o *fetchOptions) error { if o.authMethod != authMethodDefault { return federationErr.New("cannot use both SPIFFE and Web PKI authentication") } o.transport.TLSClientConfig = tlsconfig.TLSClientConfig(bundleSource, tlsconfig.AuthorizeID(endpointID)) o.authMethod = authMethodSPIFFE return nil }) } // WithWebPKIRoots authenticates the bundle endpoint using Web PKI authentication // using the provided X.509 root certificates instead of the system ones. This option // cannot be used in conjuntion with WithSPIFFEAuth option. func WithWebPKIRoots(rootCAs *x509.CertPool) FetchOption { return fetchOption(func(o *fetchOptions) error { if o.authMethod != authMethodDefault { return federationErr.New("cannot use both SPIFFE and Web PKI authentication") } o.transport.TLSClientConfig = &tls.Config{ RootCAs: rootCAs, MinVersion: tls.VersionTLS12, } o.authMethod = authMethodWebPKI return nil }) } // FetchBundle retrieves a bundle from a bundle endpoint. func FetchBundle(ctx context.Context, trustDomain spiffeid.TrustDomain, url string, option ...FetchOption) (*spiffebundle.Bundle, error) { opts := fetchOptions{ transport: http.DefaultTransport.(*http.Transport).Clone(), } for _, o := range option { if err := o.apply(&opts); err != nil { return nil, err } } var client = &http.Client{ Transport: opts.transport, } request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return nil, federationErr.New("could not create request: %w", err) } response, err := client.Do(request) if err != nil { return nil, federationErr.New("could not GET bundle: %w", err) } defer response.Body.Close() bundle, err := spiffebundle.Read(trustDomain, response.Body) if err != nil { return nil, federationErr.Wrap(err) } return bundle, nil } type fetchOption func(*fetchOptions) error func (fo fetchOption) apply(opts *fetchOptions) error { return fo(opts) } type authMethod int const ( authMethodDefault authMethod = iota authMethodSPIFFE authMethodWebPKI ) golang-github-spiffe-go-spiffe-2.4.0/v2/federation/fetch_test.go000066400000000000000000000077271474173014300245420ustar00rootroot00000000000000package federation_test import ( "context" "net" "testing" "github.com/spiffe/go-spiffe/v2/federation" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakebundleendpoint" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" ) var ( td = spiffeid.RequireTrustDomainFromString("domain.test") localhostIPs = []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback} ) func TestFetchBundle_WebPKIRoots(t *testing.T) { ca := test.NewCA(t, td) bundle := ca.Bundle() be := fakebundleendpoint.New(t, fakebundleendpoint.WithTestBundles(bundle)) defer be.Shutdown() fetchedBundle, err := federation.FetchBundle(context.Background(), td, be.FetchBundleURL(), federation.WithWebPKIRoots(be.RootCAs())) assert.NoError(t, err) assert.Equal(t, fetchedBundle, bundle) } func TestFetchBundle_SPIFFEAuth(t *testing.T) { id := spiffeid.RequireFromPath(td, "/control-plane/test-bundle-endpoint") ca := test.NewCA(t, td) svid := ca.CreateX509SVID(id, test.WithIPAddresses(localhostIPs...)) bundle := ca.Bundle() be := fakebundleendpoint.New(t, fakebundleendpoint.WithTestBundles(bundle), fakebundleendpoint.WithSPIFFEAuth(bundle, svid)) defer be.Shutdown() fetchedBundle, err := federation.FetchBundle(context.Background(), td, be.FetchBundleURL(), federation.WithSPIFFEAuth(bundle, id)) assert.NoError(t, err) assert.Equal(t, fetchedBundle, bundle) } func TestFetchBundle_SPIFFEAuth_UnexpectedID(t *testing.T) { id := spiffeid.RequireFromPath(td, "/control-plane/test-bundle-endpoint") ca := test.NewCA(t, td) svid := ca.CreateX509SVID(id, test.WithIPAddresses(localhostIPs...)) bundle := ca.Bundle() be := fakebundleendpoint.New(t, fakebundleendpoint.WithTestBundles(bundle), fakebundleendpoint.WithSPIFFEAuth(bundle, svid)) defer be.Shutdown() fetchedBundle, err := federation.FetchBundle(context.Background(), td, be.FetchBundleURL(), federation.WithSPIFFEAuth(bundle, spiffeid.RequireFromPath(td, "/other/id"))) assert.Regexp(t, `federation: could not GET bundle: Get "?`+be.FetchBundleURL()+`"?: unexpected ID "spiffe://domain.test/control-plane/test-bundle-endpoint"`, err.Error()) assert.Nil(t, fetchedBundle) } func TestFetchBundle_SPIFFEAuthAndWebPKIRoots(t *testing.T) { fetchedBundle, err := federation.FetchBundle(context.Background(), td, "url not used", federation.WithSPIFFEAuth(nil, spiffeid.ID{}), federation.WithWebPKIRoots(nil)) assert.EqualError(t, err, `federation: cannot use both SPIFFE and Web PKI authentication`) assert.Nil(t, fetchedBundle) } func TestFetchBundle_WebPKIRootsAndSPIFFEAuth(t *testing.T) { fetchedBundle, err := federation.FetchBundle(context.Background(), td, "url not used", federation.WithWebPKIRoots(nil), federation.WithSPIFFEAuth(nil, spiffeid.ID{})) assert.EqualError(t, err, `federation: cannot use both SPIFFE and Web PKI authentication`) assert.Nil(t, fetchedBundle) } func TestFetchBundle_ErrorCreatingRequest(t *testing.T) { fetchedBundle, err := federation.FetchBundle(nil, td, "url not used") //nolint assert.EqualError(t, err, `federation: could not create request: net/http: nil Context`) assert.Nil(t, fetchedBundle) } func TestFetchBundle_ErrorGettingBundle(t *testing.T) { be := fakebundleendpoint.New(t) defer be.Shutdown() ctx, cancel := context.WithCancel(context.Background()) cancel() fetchedBundle, err := federation.FetchBundle(ctx, td, be.FetchBundleURL(), federation.WithWebPKIRoots(be.RootCAs())) assert.Regexp(t, `federation: could not GET bundle: Get "?`+be.FetchBundleURL()+`"?: context canceled`, err.Error()) assert.Nil(t, fetchedBundle) } func TestFetchBundle_ErrorReadingBundleBody(t *testing.T) { be := fakebundleendpoint.New(t) defer be.Shutdown() fetchedBundle, err := federation.FetchBundle(context.Background(), td, be.FetchBundleURL(), federation.WithWebPKIRoots(be.RootCAs())) assert.EqualError(t, err, `federation: spiffebundle: unable to parse JWKS: unexpected end of JSON input`) assert.Nil(t, fetchedBundle) } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/handler.go000066400000000000000000000042541474173014300240170ustar00rootroot00000000000000package federation import ( "fmt" "net/http" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/logger" "github.com/spiffe/go-spiffe/v2/spiffeid" ) type HandlerOption interface { apply(*handlerConfig) error } func WithLogger(log logger.Logger) HandlerOption { return handlerOption(func(c *handlerConfig) error { c.log = log return nil }) } // NewHandler returns an HTTP handler that provides the trust domain bundle for // the given trust domain. The bundle is encoded according to the format // outlined in the SPIFFE Trust Domain and Bundle specification. The bundle // source is used to obtain the bundle on each request. Source implementations // should consider a caching strategy if retrieval is expensive. // See the specification for more details: // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md func NewHandler(trustDomain spiffeid.TrustDomain, source spiffebundle.Source, opts ...HandlerOption) (http.Handler, error) { conf := &handlerConfig{ log: logger.Null, } for _, opt := range opts { if err := opt.apply(conf); err != nil { return nil, fmt.Errorf("handler configuration is invalid: %w", err) } } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method is not allowed", http.StatusMethodNotAllowed) return } bundle, err := source.GetBundleForTrustDomain(trustDomain) if err != nil { conf.log.Errorf("unable to get bundle for trust domain %q: %v", trustDomain, err) http.Error(w, fmt.Sprintf("unable to serve bundle for %q", trustDomain), http.StatusInternalServerError) return } data, err := bundle.Marshal() if err != nil { conf.log.Errorf("unable to marshal bundle for trust domain %q: %v", trustDomain, err) http.Error(w, fmt.Sprintf("unable to serve bundle for %q", trustDomain), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(data) }), nil } type handlerConfig struct { log logger.Logger } type handlerOption func(*handlerConfig) error func (o handlerOption) apply(conf *handlerConfig) error { return o(conf) } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/handler_test.go000066400000000000000000000112771474173014300250610ustar00rootroot00000000000000package federation_test import ( "bytes" "crypto/x509" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/federation" "github.com/spiffe/go-spiffe/v2/logger" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/require" ) const jwks = `{ "keys": [ { "use": "x509-svid", "kty": "EC", "crv": "P-256", "x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4", "y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI", "x5c": [ "MIIBKjCB0aADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHyvsCk5yi+yhSzNu5aquQwvm8a1Wh+qw1fiHAkhDni+wq+g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KKjODA2MA8GA1UdEwEB/wQFMAMBAf8wIwYDVR0RAQH/BBkwF4YVc3BpZmZlOi8vZG9tYWluMS50ZXN0MAoGCCqGSM49BAMCA0gAMEUCIA2dO09Xmakw2ekuHKWC4hBhCkpr5qY4bI8YUcXfxg/1AiEA67kMyH7bQnr7OVLUrL+b9ylAdZglS5kKnYigmwDh+/U=" ] }, { "use": "jwt-svid", "kty": "EC", "kid": "KID", "crv": "P-256", "x": "fK-wKTnKL7KFLM27lqq5DC-bxrVaH6rDV-IcCSEOeL4", "y": "wq-g3TQWxYlV51TCPH030yXsRxvujD4hUUaIQrXk4KI" } ] }` func TestHandler(t *testing.T) { trustDomain, err := spiffeid.TrustDomainFromString("test.domain") require.NoError(t, err) bundle, err := spiffebundle.Parse(trustDomain, []byte(jwks)) require.NoError(t, err) writer := new(bytes.Buffer) source := &fakeSource{} handler, err := federation.NewHandler(trustDomain, source, federation.WithLogger(logger.Writer(writer))) require.NoError(t, err) server := httptest.NewServer(handler) defer server.Close() testCases := []struct { name string response string statusCode int log string call func(server *httptest.Server) (*http.Response, error) }{ { name: "success x509 bundle", call: func(server *httptest.Server) (response *http.Response, err error) { source.bundles = map[spiffeid.TrustDomain]*spiffebundle.Bundle{ trustDomain: bundle, } return http.Get(server.URL) }, statusCode: http.StatusOK, response: jwks, }, { name: "invalid method", call: func(server *httptest.Server) (response *http.Response, err error) { source.bundles = map[spiffeid.TrustDomain]*spiffebundle.Bundle{ trustDomain: {}, } return http.Post(server.URL, "application/json", strings.NewReader("test")) }, statusCode: http.StatusMethodNotAllowed, response: "method is not allowed\n", }, { name: "bundle not found", call: func(server *httptest.Server) (response *http.Response, err error) { source.bundles = map[spiffeid.TrustDomain]*spiffebundle.Bundle{ spiffeid.RequireTrustDomainFromString("test.domain2"): {}, } return http.Get(server.URL) }, statusCode: http.StatusInternalServerError, response: "unable to serve bundle for \"test.domain\"\n", log: "unable to get bundle for trust domain \"test.domain\": bundle not found", }, { name: "marshaling error", call: func(server *httptest.Server) (response *http.Response, err error) { source.bundles = map[spiffeid.TrustDomain]*spiffebundle.Bundle{ // Create an invalid bundle trustDomain: spiffebundle.FromX509Authorities(trustDomain, []*x509.Certificate{ { Raw: []byte("invalid raw"), }, }), } return http.Get(server.URL) }, statusCode: http.StatusInternalServerError, response: "unable to serve bundle for \"test.domain\"\n", log: "unable to marshal bundle for trust domain \"test.domain\": json: error calling MarshalJSON", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { writer.Reset() res, err := testCase.call(server) require.NoError(t, err) defer res.Body.Close() actual, err := io.ReadAll(res.Body) require.NoError(t, err) switch { case res.StatusCode == http.StatusOK: require.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) require.JSONEq(t, testCase.response, string(actual)) default: require.Equal(t, testCase.statusCode, res.StatusCode) require.Equal(t, testCase.response, string(actual)) if testCase.log != "" { require.Contains(t, writer.String(), testCase.log) } } }) } } type fakeSource struct { bundles map[spiffeid.TrustDomain]*spiffebundle.Bundle } func (s *fakeSource) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*spiffebundle.Bundle, error) { b, ok := s.bundles[trustDomain] if !ok { return nil, errors.New("bundle not found") } return b, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/watch.go000066400000000000000000000047661474173014300235200ustar00rootroot00000000000000package federation import ( "context" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // BundleWatcher is used by WatchBundle to provide the caller with bundle updates and // control the next refresh time. type BundleWatcher interface { // NextRefresh is called by WatchBundle to determine when the next refresh // should take place. A refresh hint is provided, which can be zero, meaning // the watcher is free to choose its own refresh cadence. If the refresh hint // is greater than zero, the watcher SHOULD return a next refresh time at or // below that to ensure the bundle stays up-to-date. NextRefresh(refreshHint time.Duration) time.Duration // OnUpdate is called when a bundle has been updated. If a bundle is // fetched but has not changed from the previously fetched bundle, OnUpdate // will not be called. This function is called synchronously by WatchBundle // and therefore should have a short execution time to prevent blocking the // watch. OnUpdate(*spiffebundle.Bundle) // OnError is called if there is an error fetching the bundle from the // endpoint. This function is called synchronously by WatchBundle // and therefore should have a short execution time to prevent blocking the // watch. OnError(err error) } // WatchBundle watches a bundle on a bundle endpoint. It returns when the // context is canceled, returning ctx.Err(). func WatchBundle(ctx context.Context, trustDomain spiffeid.TrustDomain, url string, watcher BundleWatcher, options ...FetchOption) error { if watcher == nil { return federationErr.New("watcher cannot be nil") } latestBundle := &spiffebundle.Bundle{} var timer *time.Timer for { bundle, err := FetchBundle(ctx, trustDomain, url, options...) switch { // Context was canceled when fetching bundle, so to avoid // more calls to FetchBundle (because the timer could be expired at // this point) we return now. case ctx.Err() == context.Canceled: return ctx.Err() case err != nil: watcher.OnError(err) case !latestBundle.Equal(bundle): watcher.OnUpdate(bundle) latestBundle = bundle } var nextRefresh time.Duration if refreshHint, ok := latestBundle.RefreshHint(); ok { nextRefresh = watcher.NextRefresh(refreshHint) } else { nextRefresh = watcher.NextRefresh(0) } if timer == nil { timer = time.NewTimer(nextRefresh) defer timer.Stop() } else { timer.Reset(nextRefresh) } select { case <-timer.C: case <-ctx.Done(): return ctx.Err() } } } golang-github-spiffe-go-spiffe-2.4.0/v2/federation/watch_test.go000066400000000000000000000062271474173014300245510ustar00rootroot00000000000000package federation_test import ( "context" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/federation" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakebundleendpoint" "github.com/stretchr/testify/assert" ) func TestWatchBundle_OnUpdate(t *testing.T) { var watcher *fakewatcher ca1 := test.NewCA(t, td) bundle1 := ca1.Bundle() bundle1.SetRefreshHint(time.Second) ca2 := test.NewCA(t, td) bundle2 := ca2.Bundle() bundle2.SetRefreshHint(2 * time.Second) bundles := []*spiffebundle.Bundle{bundle1, bundle2} be := fakebundleendpoint.New(t, fakebundleendpoint.WithTestBundles(bundle1, bundle2)) defer be.Shutdown() ctx, cancel := context.WithCancel(context.Background()) watcher = &fakewatcher{ t: t, nextRefresh: time.Second, expectedBundles: bundles, cancel: func() { if watcher.onUpdateCalls > 1 { cancel() } }, latestBundle: &spiffebundle.Bundle{}, } err := federation.WatchBundle(ctx, td, be.FetchBundleURL(), watcher, federation.WithWebPKIRoots(be.RootCAs())) assert.Equal(t, 2, watcher.onUpdateCalls) assert.Equal(t, 0, watcher.onErrorCalls) assert.Equal(t, context.Canceled, err) } func TestWatchBundle_OnError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) watcher := &fakewatcher{ t: t, nextRefresh: time.Second, expectedErr: `federation: could not GET bundle: Get "?wrong%20url"?: unsupported protocol scheme ""`, cancel: cancel, latestBundle: &spiffebundle.Bundle{}, } err := federation.WatchBundle(ctx, td, "wrong url", watcher) assert.Equal(t, 0, watcher.onUpdateCalls) assert.Equal(t, 1, watcher.onErrorCalls) assert.Equal(t, context.Canceled, err) } func TestWatchBundle_NilWatcher(t *testing.T) { err := federation.WatchBundle(context.Background(), td, "some url", nil) assert.EqualError(t, err, "federation: watcher cannot be nil") } func TestWatchBundle_FetchBundleCanceled(t *testing.T) { be := fakebundleendpoint.New(t) defer be.Shutdown() ctx, cancel := context.WithCancel(context.Background()) watcher := &fakewatcher{ t: t, nextRefresh: time.Second, } cancel() err := federation.WatchBundle(ctx, td, be.FetchBundleURL(), watcher, federation.WithWebPKIRoots(be.RootCAs())) assert.Equal(t, context.Canceled, err) } type fakewatcher struct { t *testing.T nextRefresh time.Duration expectedBundles []*spiffebundle.Bundle expectedErr string cancel context.CancelFunc onUpdateCalls int onErrorCalls int latestBundle *spiffebundle.Bundle } func (w *fakewatcher) NextRefresh(refreshHint time.Duration) time.Duration { if rh, ok := w.latestBundle.RefreshHint(); ok { assert.Equal(w.t, rh, refreshHint) } else { assert.Equal(w.t, time.Duration(0), refreshHint) } return w.nextRefresh } func (w *fakewatcher) OnUpdate(bundle *spiffebundle.Bundle) { w.latestBundle = bundle assert.Equal(w.t, w.expectedBundles[w.onUpdateCalls], bundle) w.onUpdateCalls++ w.cancel() } func (w *fakewatcher) OnError(err error) { assert.Regexp(w.t, w.expectedErr, err.Error()) w.onErrorCalls++ w.cancel() } golang-github-spiffe-go-spiffe-2.4.0/v2/go.mod000066400000000000000000000015061474173014300210360ustar00rootroot00000000000000module github.com/spiffe/go-spiffe/v2 go 1.21 require ( github.com/Microsoft/go-winio v0.6.2 github.com/go-jose/go-jose/v4 v4.0.4 github.com/stretchr/testify v1.9.0 github.com/zeebo/errs v1.3.0 google.golang.org/grpc v1.67.1 google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 google.golang.org/protobuf v1.34.2 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-spiffe-go-spiffe-2.4.0/v2/go.sum000066400000000000000000000066751474173014300210770ustar00rootroot00000000000000github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/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/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-spiffe-go-spiffe-2.4.0/v2/internal/000077500000000000000000000000001474173014300215425ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/cryptoutil/000077500000000000000000000000001474173014300237605ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/cryptoutil/keys.go000066400000000000000000000015351474173014300252660ustar00rootroot00000000000000package cryptoutil import ( "bytes" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "fmt" ) func PublicKeyEqual(a, b crypto.PublicKey) (bool, error) { switch a := a.(type) { case *rsa.PublicKey: rsaPublicKey, ok := b.(*rsa.PublicKey) return ok && RSAPublicKeyEqual(a, rsaPublicKey), nil case *ecdsa.PublicKey: ecdsaPublicKey, ok := b.(*ecdsa.PublicKey) return ok && ECDSAPublicKeyEqual(a, ecdsaPublicKey), nil case ed25519.PublicKey: ed25519PublicKey, ok := b.(ed25519.PublicKey) return ok && bytes.Equal(a, ed25519PublicKey), nil default: return false, fmt.Errorf("unsupported public key type %T", a) } } func RSAPublicKeyEqual(a, b *rsa.PublicKey) bool { return a.E == b.E && a.N.Cmp(b.N) == 0 } func ECDSAPublicKeyEqual(a, b *ecdsa.PublicKey) bool { return a.Curve == b.Curve && a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/jwtutil/000077500000000000000000000000001474173014300232445ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/jwtutil/util.go000066400000000000000000000013341474173014300245510ustar00rootroot00000000000000package jwtutil import ( "crypto" "github.com/spiffe/go-spiffe/v2/internal/cryptoutil" ) // CopyJWTAuthorities copies JWT authorities from a map to a new map. func CopyJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) map[string]crypto.PublicKey { copiedJWTAuthorities := make(map[string]crypto.PublicKey) for key, jwtAuthority := range jwtAuthorities { copiedJWTAuthorities[key] = jwtAuthority } return copiedJWTAuthorities } func JWTAuthoritiesEqual(a, b map[string]crypto.PublicKey) bool { if len(a) != len(b) { return false } for k, pka := range a { pkb, ok := b[k] if !ok { return false } if equal, _ := cryptoutil.PublicKeyEqual(pka, pkb); !equal { return false } } return true } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/pemutil/000077500000000000000000000000001474173014300232215ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/pemutil/pem.go000066400000000000000000000050621474173014300243340ustar00rootroot00000000000000package pemutil import ( "crypto" "crypto/x509" "encoding/pem" "errors" "fmt" ) const ( certType string = "CERTIFICATE" keyType string = "PRIVATE KEY" ) func ParseCertificates(certsBytes []byte) ([]*x509.Certificate, error) { objects, err := parseBlocks(certsBytes, certType) if err != nil { return nil, err } certs := []*x509.Certificate{} for _, object := range objects { cert, ok := object.(*x509.Certificate) if !ok { return nil, fmt.Errorf("expected *x509.Certificate; got %T", object) } certs = append(certs, cert) } return certs, nil } func ParsePrivateKey(keyBytes []byte) (crypto.PrivateKey, error) { objects, err := parseBlocks(keyBytes, keyType) if err != nil { return nil, err } if len(objects) == 0 { return nil, nil } privateKey, ok := objects[0].(crypto.PrivateKey) if !ok { return nil, fmt.Errorf("expected crypto.PrivateKey; got %T", objects[0]) } return privateKey, nil } func EncodePKCS8PrivateKey(privateKey interface{}) ([]byte, error) { keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{ Type: keyType, Bytes: keyBytes, }), nil } func EncodeCertificates(certificates []*x509.Certificate) []byte { pemBytes := []byte{} for _, cert := range certificates { pemBytes = append(pemBytes, pem.EncodeToMemory(&pem.Block{ Type: certType, Bytes: cert.Raw, })...) } return pemBytes } func parseBlocks(blocksBytes []byte, expectedType string) ([]interface{}, error) { objects := []interface{}{} var foundBlocks = false for { if len(blocksBytes) == 0 { if len(objects) == 0 && !foundBlocks { return nil, errors.New("no PEM blocks found") } return objects, nil } object, rest, foundBlock, err := parseBlock(blocksBytes, expectedType) blocksBytes = rest if foundBlock { foundBlocks = true } switch { case err != nil: return nil, err case object != nil: objects = append(objects, object) } } } func parseBlock(pemBytes []byte, pemType string) (interface{}, []byte, bool, error) { pemBlock, rest := pem.Decode(pemBytes) if pemBlock == nil { return nil, nil, false, nil } if pemBlock.Type != pemType { return nil, rest, true, nil } var object interface{} var err error switch pemType { case certType: object, err = x509.ParseCertificate(pemBlock.Bytes) case keyType: object, err = x509.ParsePKCS8PrivateKey(pemBlock.Bytes) default: err = fmt.Errorf("PEM type not supported: %q", pemType) } if err != nil { return nil, nil, false, err } return object, rest, true, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/pemutil/pem_test.go000066400000000000000000000076421474173014300254010ustar00rootroot00000000000000package pemutil_test import ( "bytes" "crypto/x509" "encoding/pem" "testing" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/stretchr/testify/require" ) var ( testCertsPEM = []byte(`-----BEGIN CERTIFICATE----- MIH0MIGboAMCAQICAQEwCgYIKoZIzj0EAwIwADAiGA8wMDAxMDEwMTAwMDAwMFoY DzAwMDEwMTAxMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElb3Z CIuzbVMQsWP1b0snmJxNkpG9xT8ZHt88byGhJsde2y0zoOld9+soPZpjLBehx2Wf 6pTc22/r61HCoIFkfaMCMAAwCgYIKoZIzj0EAwIDSAAwRQIhANztuW3qmu/UfoXQ 97bYXmIunEIRPSowxAcruqO46GqhAiBSxPFst6yb3cIRwDnr4rBfaUb13NigI1iK TM0VOlcTxg== -----END CERTIFICATE----- `) testCerts, _ = x509.ParseCertificates(pemBlockData(testCertsPEM)) testKeyPEM = []byte(`-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsswWk0ZyjTDMD7zL zUFjYzbfrouQgIAitSmJMnHQcyqhRANCAASVvdkIi7NtUxCxY/VvSyeYnE2Skb3F Pxke3zxvIaEmx17bLTOg6V336yg9mmMsF6HHZZ/qlNzbb+vrUcKggWR9 -----END PRIVATE KEY----- `) testKey, _ = x509.ParsePKCS8PrivateKey(pemBlockData(testKeyPEM)) ) func TestEncodeCertificates(t *testing.T) { actualPEM := pemutil.EncodeCertificates(testCerts) require.Equal(t, testCertsPEM, actualPEM) } func TestEncodePKCSPrivateKey(t *testing.T) { actualPEM, err := pemutil.EncodePKCS8PrivateKey(testKey) require.NoError(t, err) require.Equal(t, testKeyPEM, actualPEM) } func TestParseCertificates(t *testing.T) { filler := []byte("filler\n") t.Run("empty", func(t *testing.T) { _, err := pemutil.ParseCertificates(nil) require.EqualError(t, err, "no PEM blocks found") }) t.Run("only filler", func(t *testing.T) { _, err := pemutil.ParseCertificates(filler) require.EqualError(t, err, "no PEM blocks found") }) t.Run("without filler", func(t *testing.T) { certs, err := pemutil.ParseCertificates(testCertsPEM) require.NoError(t, err) require.Equal(t, testCerts, certs) }) t.Run("with filler", func(t *testing.T) { certs, err := pemutil.ParseCertificates(concatBytes(filler, testCertsPEM, filler)) require.NoError(t, err) require.Equal(t, testCerts, certs) }) t.Run("before key", func(t *testing.T) { certs, err := pemutil.ParseCertificates(concatBytes(testCertsPEM, testKeyPEM)) require.NoError(t, err) require.Equal(t, testCerts, certs) }) t.Run("after key", func(t *testing.T) { certs, err := pemutil.ParseCertificates(concatBytes(testKeyPEM, testCertsPEM)) require.NoError(t, err) require.Equal(t, testCerts, certs) }) } func TestParsePrivateKey(t *testing.T) { filler := []byte("filler\n") t.Run("empty", func(t *testing.T) { _, err := pemutil.ParsePrivateKey(nil) require.EqualError(t, err, "no PEM blocks found") }) t.Run("only filler", func(t *testing.T) { _, err := pemutil.ParsePrivateKey(filler) require.EqualError(t, err, "no PEM blocks found") }) t.Run("without filler", func(t *testing.T) { key, err := pemutil.ParsePrivateKey(testKeyPEM) require.NoError(t, err) require.Equal(t, testKey, key) }) t.Run("with filler", func(t *testing.T) { key, err := pemutil.ParsePrivateKey(concatBytes(filler, testKeyPEM, filler)) require.NoError(t, err) require.Equal(t, testKey, key) }) t.Run("before certificate", func(t *testing.T) { key, err := pemutil.ParsePrivateKey(concatBytes(testKeyPEM, testCertsPEM)) require.NoError(t, err) require.Equal(t, testKey, key) }) t.Run("after certificate", func(t *testing.T) { key, err := pemutil.ParsePrivateKey(concatBytes(testCertsPEM, testKeyPEM)) require.NoError(t, err) require.Equal(t, testKey, key) }) } func pemBlockData(data []byte) (out []byte) { blocks, _ := decodePEM(data) for _, block := range blocks { if block != nil { out = append(out, block.Bytes...) } } return out } func decodePEM(data []byte) (blocks []*pem.Block, extra [][]byte) { for { block, rest := pem.Decode(data) blocks = append(blocks, block) extra = append(extra, rest) if block == nil { return blocks, extra } data = rest } } func concatBytes(bs ...[]byte) []byte { return bytes.Join(bs, nil) } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/000077500000000000000000000000001474173014300225215ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/ca.go000066400000000000000000000202771474173014300234430ustar00rootroot00000000000000package test import ( "crypto" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "fmt" "math/big" "net" "net/url" "testing" "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/cryptosigner" "github.com/go-jose/go-jose/v4/jwt" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/require" ) var localhostIPs = []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback} type CA struct { tb testing.TB td spiffeid.TrustDomain parent *CA cert *x509.Certificate key crypto.Signer jwtKey crypto.Signer jwtKid string } func NewCA(tb testing.TB, td spiffeid.TrustDomain) *CA { cert, key := CreateCACertificate(tb, nil, nil) return &CA{ tb: tb, td: td, cert: cert, key: key, jwtKey: NewEC256Key(tb), jwtKid: NewKeyID(tb), } } func (ca *CA) ChildCA(options ...SVIDOption) *CA { cert, key := CreateCACertificate(ca.tb, ca.cert, ca.key, options...) return &CA{ tb: ca.tb, parent: ca, cert: cert, key: key, jwtKey: NewEC256Key(ca.tb), jwtKid: NewKeyID(ca.tb), } } func (ca *CA) CreateX509SVID(id spiffeid.ID, options ...SVIDOption) *x509svid.SVID { cert, key := CreateX509SVID(ca.tb, ca.cert, ca.key, id, options...) svid := &x509svid.SVID{ ID: id, Certificates: append([]*x509.Certificate{cert}, ca.chain(false)...), PrivateKey: key, } applyX509SVIDOptions(svid, options...) return svid } func (ca *CA) CreateX509Certificate(options ...SVIDOption) ([]*x509.Certificate, crypto.Signer) { cert, key := CreateX509Certificate(ca.tb, ca.cert, ca.key, options...) return append([]*x509.Certificate{cert}, ca.chain(false)...), key } func (ca *CA) CreateJWTSVID(id spiffeid.ID, audience []string, options ...SVIDOption) *jwtsvid.SVID { claims := jwt.Claims{ Subject: id.String(), Issuer: "FAKECA", Audience: audience, IssuedAt: jwt.NewNumericDate(time.Now()), Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), } jwtSigner, err := jose.NewSigner( jose.SigningKey{ Algorithm: jose.ES256, Key: jose.JSONWebKey{ Key: cryptosigner.Opaque(ca.jwtKey), KeyID: ca.jwtKid, }, }, new(jose.SignerOptions).WithType("JWT"), ) require.NoError(ca.tb, err) signedToken, err := jwt.Signed(jwtSigner).Claims(claims).Serialize() require.NoError(ca.tb, err) svid, err := jwtsvid.ParseInsecure(signedToken, audience) require.NoError(ca.tb, err) applyJWTSVIDOptions(svid, options...) return svid } func (ca *CA) X509Authorities() []*x509.Certificate { root := ca for root.parent != nil { root = root.parent } return []*x509.Certificate{root.cert} } func (ca *CA) JWTAuthorities() map[string]crypto.PublicKey { return map[string]crypto.PublicKey{ ca.jwtKid: ca.jwtKey.Public(), } } func (ca *CA) Bundle() *spiffebundle.Bundle { bundle := spiffebundle.New(ca.td) bundle.SetX509Authorities(ca.X509Authorities()) bundle.SetJWTAuthorities(ca.JWTAuthorities()) return bundle } func (ca *CA) X509Bundle() *x509bundle.Bundle { return x509bundle.FromX509Authorities(ca.td, ca.X509Authorities()) } func (ca *CA) JWTBundle() *jwtbundle.Bundle { return jwtbundle.FromJWTAuthorities(ca.td, ca.JWTAuthorities()) } func CreateCACertificate(tb testing.TB, parent *x509.Certificate, parentKey crypto.Signer, options ...SVIDOption) (*x509.Certificate, crypto.Signer) { now := time.Now() serial := NewSerial(tb) key := NewEC256Key(tb) tmpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: fmt.Sprintf("CA %x", serial), }, BasicConstraintsValid: true, IsCA: true, NotBefore: now, NotAfter: now.Add(time.Hour), } applyCertOptions(tmpl, options...) if parent == nil { parent = tmpl parentKey = key } return CreateCertificate(tb, tmpl, parent, key.Public(), parentKey), key } func CreateX509Certificate(tb testing.TB, parent *x509.Certificate, parentKey crypto.Signer, options ...SVIDOption) (*x509.Certificate, crypto.Signer) { now := time.Now() serial := NewSerial(tb) key := NewEC256Key(tb) tmpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: fmt.Sprintf("X509-Certificate %x", serial), }, NotBefore: now, NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, } applyCertOptions(tmpl, options...) return CreateCertificate(tb, tmpl, parent, key.Public(), parentKey), key } func CreateX509SVID(tb testing.TB, parent *x509.Certificate, parentKey crypto.Signer, id spiffeid.ID, options ...SVIDOption) (*x509.Certificate, crypto.Signer) { serial := NewSerial(tb) options = append(options, WithSerial(serial), WithKeyUsage(x509.KeyUsageDigitalSignature), WithSubject(pkix.Name{ CommonName: fmt.Sprintf("X509-SVID %x", serial), }), WithURIs(id.URL())) return CreateX509Certificate(tb, parent, parentKey, options...) } func CreateCertificate(tb testing.TB, tmpl, parent *x509.Certificate, pub, priv interface{}) *x509.Certificate { certDER, err := x509.CreateCertificate(rand.Reader, tmpl, parent, pub, priv) require.NoError(tb, err) cert, err := x509.ParseCertificate(certDER) require.NoError(tb, err) return cert } func CreateWebCredentials(t testing.TB) (*x509.CertPool, *tls.Certificate) { rootCert, rootKey := CreateCACertificate(t, nil, nil) childCert, childKey := CreateX509Certificate(t, rootCert, rootKey, WithIPAddresses(localhostIPs...)) return x509util.NewCertPool([]*x509.Certificate{rootCert}), &tls.Certificate{ Certificate: [][]byte{childCert.Raw}, PrivateKey: childKey, } } func NewSerial(tb testing.TB) *big.Int { b := make([]byte, 8) _, err := rand.Read(b) require.NoError(tb, err) return new(big.Int).SetBytes(b) } type SVIDOption struct { certificateOption func(*x509.Certificate) x509SvidOption func(*x509svid.SVID) jwtSvidOption func(*jwtsvid.SVID) } func (s SVIDOption) applyJWTSVIDOption(svid *jwtsvid.SVID) { if s.jwtSvidOption != nil { s.jwtSvidOption(svid) } } func (s SVIDOption) applyCertOption(certificate *x509.Certificate) { if s.certificateOption != nil { s.certificateOption(certificate) } } func (s SVIDOption) applyX509SVIDOption(svid *x509svid.SVID) { if s.x509SvidOption != nil { s.x509SvidOption(svid) } } func WithSerial(serial *big.Int) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.SerialNumber = serial }, } } func WithKeyUsage(keyUsage x509.KeyUsage) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.KeyUsage = keyUsage }, } } func WithLifetime(notBefore, notAfter time.Time) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.NotBefore = notBefore c.NotAfter = notAfter }, } } func WithIPAddresses(ips ...net.IP) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.IPAddresses = ips }, } } func WithURIs(uris ...*url.URL) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.URIs = uris }, } } func WithSubject(subject pkix.Name) SVIDOption { return SVIDOption{ certificateOption: func(c *x509.Certificate) { c.Subject = subject }, } } func WithHint(hint string) SVIDOption { return SVIDOption{ x509SvidOption: func(svid *x509svid.SVID) { svid.Hint = hint }, jwtSvidOption: func(svid *jwtsvid.SVID) { svid.Hint = hint }, } } func applyCertOptions(c *x509.Certificate, options ...SVIDOption) { for _, opt := range options { opt.applyCertOption(c) } } func applyX509SVIDOptions(svid *x509svid.SVID, options ...SVIDOption) { for _, opt := range options { opt.applyX509SVIDOption(svid) } } func applyJWTSVIDOptions(svid *jwtsvid.SVID, options ...SVIDOption) { for _, opt := range options { opt.applyJWTSVIDOption(svid) } } func (ca *CA) chain(includeRoot bool) []*x509.Certificate { chain := []*x509.Certificate{} next := ca for next != nil { if includeRoot || next.parent != nil { chain = append(chain, next.cert) } next = next.parent } return chain } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/errstrings/000077500000000000000000000000001474173014300247235ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/errstrings/err_posix.go000066400000000000000000000002151474173014300272620ustar00rootroot00000000000000//go:build !windows // +build !windows // OS specific error strings package errstrings var ( FileNotFound = "no such file or directory" ) golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/errstrings/err_windows.go000066400000000000000000000002341474173014300276130ustar00rootroot00000000000000//go:build windows // +build windows // OS specific error strings package errstrings var ( FileNotFound = "The system cannot find the file specified." ) golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakebundleendpoint/000077500000000000000000000000001474173014300263625ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakebundleendpoint/server.go000066400000000000000000000061221474173014300302200ustar00rootroot00000000000000package fakebundleendpoint import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "net/http" "sync" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" ) type Server struct { tb testing.TB wg sync.WaitGroup addr net.Addr httpServer *http.Server // Root certificates used by clients to verify server certificates. rootCAs *x509.CertPool // TLS configuration used by the server. tlscfg *tls.Config // SPIFFE bundles that can be returned by this Server. bundles []*spiffebundle.Bundle } type ServerOption interface { apply(*Server) } func New(tb testing.TB, option ...ServerOption) *Server { rootCAs, cert := test.CreateWebCredentials(tb) tlscfg := &tls.Config{ Certificates: []tls.Certificate{*cert}, MinVersion: tls.VersionTLS12, } s := &Server{ tb: tb, rootCAs: rootCAs, tlscfg: tlscfg, } for _, opt := range option { opt.apply(s) } sm := http.NewServeMux() sm.HandleFunc("/test-bundle", s.testbundle) s.httpServer = &http.Server{ Handler: sm, TLSConfig: s.tlscfg, ReadHeaderTimeout: time.Second * 10, } err := s.start() if err != nil { tb.Fatalf("Failed to start: %v", err) } return s } func (s *Server) Shutdown() { err := s.httpServer.Shutdown(context.Background()) assert.NoError(s.tb, err) s.wg.Wait() } func (s *Server) Addr() string { return s.addr.String() } func (s *Server) FetchBundleURL() string { return fmt.Sprintf("https://%s/test-bundle", s.Addr()) } func (s *Server) RootCAs() *x509.CertPool { return s.rootCAs } func (s *Server) start() error { ln, err := net.Listen("tcp", "127.0.0.1:") if err != nil { return err } s.addr = ln.Addr() s.wg.Add(1) go func() { err := s.httpServer.ServeTLS(ln, "", "") assert.EqualError(s.tb, err, http.ErrServerClosed.Error()) s.wg.Done() ln.Close() }() return nil } func (s *Server) testbundle(w http.ResponseWriter, r *http.Request) { if len(s.bundles) == 0 { w.WriteHeader(http.StatusNotFound) return } bb, err := s.bundles[0].Marshal() assert.NoError(s.tb, err) s.bundles = s.bundles[1:] w.Header().Add("Content-Type", "application/json") b, err := w.Write(bb) assert.NoError(s.tb, err) assert.Equal(s.tb, len(bb), b) } type serverOption func(*Server) // WithTestBundles sets the bundles that are returned by the Bundle Endpoint. You can // specify several bundles, which are going to be returned one at a time each time // a bundle is GET by a client. func WithTestBundles(bundles ...*spiffebundle.Bundle) ServerOption { return serverOption(func(s *Server) { s.bundles = bundles }) } func WithSPIFFEAuth(bundle *spiffebundle.Bundle, svid *x509svid.SVID) ServerOption { return serverOption(func(s *Server) { s.rootCAs = x509util.NewCertPool(bundle.X509Authorities()) s.tlscfg = tlsconfig.TLSServerConfig(svid) }) } func (so serverOption) apply(s *Server) { so(s) } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakeworkloadapi/000077500000000000000000000000001474173014300256645ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakeworkloadapi/workload_api.go000066400000000000000000000236461474173014300307010ustar00rootroot00000000000000package fakeworkloadapi import ( "context" "crypto/x509" "encoding/json" "errors" "fmt" "sync" "testing" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) var noIdentityError = status.Error(codes.PermissionDenied, "no identity issued") type WorkloadAPI struct { tb testing.TB wg sync.WaitGroup addr string server *grpc.Server mu sync.Mutex x509Resp *workload.X509SVIDResponse x509Chans map[chan *workload.X509SVIDResponse]struct{} jwtResp *workload.JWTSVIDResponse jwtBundlesResp *workload.JWTBundlesResponse jwtBundlesChans map[chan *workload.JWTBundlesResponse]struct{} x509BundlesResp *workload.X509BundlesResponse x509BundlesChans map[chan *workload.X509BundlesResponse]struct{} } func New(tb testing.TB) *WorkloadAPI { w := &WorkloadAPI{ x509Chans: make(map[chan *workload.X509SVIDResponse]struct{}), jwtBundlesChans: make(map[chan *workload.JWTBundlesResponse]struct{}), x509BundlesChans: make(map[chan *workload.X509BundlesResponse]struct{}), } listener, err := newListener(tb) require.NoError(tb, err) server := grpc.NewServer() workload.RegisterSpiffeWorkloadAPIServer(server, &workloadAPIWrapper{w: w}) w.wg.Add(1) go func() { defer w.wg.Done() _ = server.Serve(listener) }() w.addr = getTargetName(listener.Addr()) tb.Logf("WorkloadAPI address: %s", w.addr) w.server = server return w } func (w *WorkloadAPI) Stop() { w.server.Stop() w.wg.Wait() } func (w *WorkloadAPI) Addr() string { return w.addr } func (w *WorkloadAPI) SetX509SVIDResponse(r *X509SVIDResponse) { var resp *workload.X509SVIDResponse if r != nil { resp = r.ToProto(w.tb) } w.mu.Lock() defer w.mu.Unlock() w.x509Resp = resp for ch := range w.x509Chans { select { case ch <- resp: default: <-ch ch <- resp } } } func (w *WorkloadAPI) SetJWTSVIDResponse(r *workload.JWTSVIDResponse) { w.mu.Lock() defer w.mu.Unlock() if r != nil { w.jwtResp = r } } func (w *WorkloadAPI) SetJWTBundles(jwtBundles ...*jwtbundle.Bundle) { resp := &workload.JWTBundlesResponse{ Bundles: make(map[string][]byte), } for _, bundle := range jwtBundles { bundleBytes, err := bundle.Marshal() assert.NoError(w.tb, err) resp.Bundles[bundle.TrustDomain().String()] = bundleBytes } w.mu.Lock() defer w.mu.Unlock() w.jwtBundlesResp = resp for ch := range w.jwtBundlesChans { select { case ch <- w.jwtBundlesResp: default: <-ch ch <- w.jwtBundlesResp } } } func (w *WorkloadAPI) SetX509Bundles(x509Bundles ...*x509bundle.Bundle) { resp := &workload.X509BundlesResponse{ Bundles: make(map[string][]byte), } for _, bundle := range x509Bundles { bundleBytes, err := bundle.Marshal() assert.NoError(w.tb, err) bundlePem, err := pemutil.ParseCertificates(bundleBytes) assert.NoError(w.tb, err) var rawBytes []byte for _, c := range bundlePem { rawBytes = append(rawBytes, c.Raw...) } resp.Bundles[bundle.TrustDomain().String()] = rawBytes } w.mu.Lock() defer w.mu.Unlock() w.x509BundlesResp = resp for ch := range w.x509BundlesChans { select { case ch <- w.x509BundlesResp: default: <-ch ch <- w.x509BundlesResp } } } type workloadAPIWrapper struct { workload.UnimplementedSpiffeWorkloadAPIServer w *WorkloadAPI } func (w *workloadAPIWrapper) FetchX509SVID(req *workload.X509SVIDRequest, stream workload.SpiffeWorkloadAPI_FetchX509SVIDServer) error { return w.w.fetchX509SVID(req, stream) } func (w *workloadAPIWrapper) FetchX509Bundles(req *workload.X509BundlesRequest, stream workload.SpiffeWorkloadAPI_FetchX509BundlesServer) error { return w.w.fetchX509Bundles(req, stream) } func (w *workloadAPIWrapper) FetchJWTSVID(ctx context.Context, req *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { return w.w.fetchJWTSVID(ctx, req) } func (w *workloadAPIWrapper) FetchJWTBundles(req *workload.JWTBundlesRequest, stream workload.SpiffeWorkloadAPI_FetchJWTBundlesServer) error { return w.w.fetchJWTBundles(req, stream) } func (w *workloadAPIWrapper) ValidateJWTSVID(ctx context.Context, req *workload.ValidateJWTSVIDRequest) (*workload.ValidateJWTSVIDResponse, error) { return w.w.validateJWTSVID(ctx, req) } type X509SVIDResponse struct { SVIDs []*x509svid.SVID Bundle *x509bundle.Bundle FederatedBundles []*x509bundle.Bundle } func (r *X509SVIDResponse) ToProto(tb testing.TB) *workload.X509SVIDResponse { var bundle []byte if r.Bundle != nil { bundle = x509util.ConcatRawCertsFromCerts(r.Bundle.X509Authorities()) } pb := &workload.X509SVIDResponse{ FederatedBundles: make(map[string][]byte), } for _, svid := range r.SVIDs { var keyDER []byte if svid.PrivateKey != nil { var err error keyDER, err = x509.MarshalPKCS8PrivateKey(svid.PrivateKey) require.NoError(tb, err) } pb.Svids = append(pb.Svids, &workload.X509SVID{ SpiffeId: svid.ID.String(), X509Svid: x509util.ConcatRawCertsFromCerts(svid.Certificates), X509SvidKey: keyDER, Bundle: bundle, Hint: svid.Hint, }) } for _, v := range r.FederatedBundles { pb.FederatedBundles[v.TrustDomain().IDString()] = x509util.ConcatRawCertsFromCerts(v.X509Authorities()) } return pb } func (w *WorkloadAPI) fetchX509SVID(_ *workload.X509SVIDRequest, stream workload.SpiffeWorkloadAPI_FetchX509SVIDServer) error { if err := checkHeader(stream.Context()); err != nil { return err } ch := make(chan *workload.X509SVIDResponse, 1) w.mu.Lock() w.x509Chans[ch] = struct{}{} resp := w.x509Resp w.mu.Unlock() defer func() { w.mu.Lock() delete(w.x509Chans, ch) w.mu.Unlock() }() sendResp := func(resp *workload.X509SVIDResponse) error { if resp == nil { return noIdentityError } return stream.Send(resp) } if err := sendResp(resp); err != nil { return err } for { select { case resp := <-ch: if err := sendResp(resp); err != nil { return err } case <-stream.Context().Done(): return stream.Context().Err() } } } func (w *WorkloadAPI) fetchX509Bundles(_ *workload.X509BundlesRequest, stream workload.SpiffeWorkloadAPI_FetchX509BundlesServer) error { if err := checkHeader(stream.Context()); err != nil { return err } ch := make(chan *workload.X509BundlesResponse, 1) w.mu.Lock() w.x509BundlesChans[ch] = struct{}{} resp := w.x509BundlesResp w.mu.Unlock() defer func() { w.mu.Lock() delete(w.x509BundlesChans, ch) w.mu.Unlock() }() sendResp := func(resp *workload.X509BundlesResponse) error { if resp == nil { return noIdentityError } return stream.Send(resp) } if err := sendResp(resp); err != nil { return err } for { select { case resp := <-ch: if err := sendResp(resp); err != nil { return err } case <-stream.Context().Done(): return stream.Context().Err() } } } func (w *WorkloadAPI) fetchJWTSVID(ctx context.Context, req *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { if err := checkHeader(ctx); err != nil { return nil, err } if len(req.Audience) == 0 { return nil, errors.New("no audience") } if w.jwtResp == nil { return nil, noIdentityError } return w.jwtResp, nil } func (w *WorkloadAPI) fetchJWTBundles(_ *workload.JWTBundlesRequest, stream workload.SpiffeWorkloadAPI_FetchJWTBundlesServer) error { if err := checkHeader(stream.Context()); err != nil { return err } ch := make(chan *workload.JWTBundlesResponse, 1) w.mu.Lock() w.jwtBundlesChans[ch] = struct{}{} resp := w.jwtBundlesResp w.mu.Unlock() defer func() { w.mu.Lock() delete(w.jwtBundlesChans, ch) w.mu.Unlock() }() sendResp := func(resp *workload.JWTBundlesResponse) error { if resp == nil { return noIdentityError } return stream.Send(resp) } if err := sendResp(resp); err != nil { return err } for { select { case resp := <-ch: if err := sendResp(resp); err != nil { return err } case <-stream.Context().Done(): return stream.Context().Err() } } } func (w *WorkloadAPI) validateJWTSVID(_ context.Context, req *workload.ValidateJWTSVIDRequest) (*workload.ValidateJWTSVIDResponse, error) { if req.Audience == "" { return nil, status.Error(codes.InvalidArgument, "audience must be specified") } if req.Svid == "" { return nil, status.Error(codes.InvalidArgument, "svid must be specified") } // TODO: validate jwtSvid, err := jwtsvid.ParseInsecure(req.Svid, []string{req.Audience}) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } claims, err := structFromValues(jwtSvid.Claims) require.NoError(w.tb, err) return &workload.ValidateJWTSVIDResponse{ SpiffeId: jwtSvid.ID.String(), Claims: claims, }, nil } func checkHeader(ctx context.Context) error { return checkMetadata(ctx, "workload.spiffe.io", "true") } func checkMetadata(ctx context.Context, key, value string) error { md, ok := metadata.FromIncomingContext(ctx) if !ok { return errors.New("request does not contain metadata") } values := md.Get(key) if len(value) == 0 { return fmt.Errorf("request metadata does not contain %q value", key) } if values[0] != value { return fmt.Errorf("request metadata %q value is %q; expected %q", key, values[0], value) } return nil } func structFromValues(values map[string]interface{}) (*structpb.Struct, error) { valuesJSON, err := json.Marshal(values) if err != nil { return nil, err } s := new(structpb.Struct) if err := protojson.Unmarshal(valuesJSON, s); err != nil { return nil, err } return s, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakeworkloadapi/workload_api_posix.go000066400000000000000000000004641474173014300321140ustar00rootroot00000000000000//go:build !windows // +build !windows package fakeworkloadapi import ( "fmt" "net" "testing" ) func newListener(_ testing.TB) (net.Listener, error) { return net.Listen("tcp", "localhost:0") } func getTargetName(addr net.Addr) string { return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/fakeworkloadapi/workload_api_windows.go000066400000000000000000000033751474173014300324500ustar00rootroot00000000000000//go:build windows // +build windows package fakeworkloadapi import ( "crypto/rand" "fmt" "math" "math/big" "net" "strings" "testing" "github.com/Microsoft/go-winio" "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) var maxUint64 = maxBigUint64() func NewWithNamedPipeListener(tb testing.TB) *WorkloadAPI { w := &WorkloadAPI{ x509Chans: make(map[chan *workload.X509SVIDResponse]struct{}), jwtBundlesChans: make(map[chan *workload.JWTBundlesResponse]struct{}), } listener, err := winio.ListenPipe(fmt.Sprintf(`\\.\pipe\go-spiffe-test-pipe-%x`, randUint64(tb)), nil) require.NoError(tb, err) server := grpc.NewServer() workload.RegisterSpiffeWorkloadAPIServer(server, &workloadAPIWrapper{w: w}) w.wg.Add(1) go func() { defer w.wg.Done() _ = server.Serve(listener) }() w.addr = getTargetName(listener.Addr()) tb.Logf("WorkloadAPI address: %s", w.addr) w.server = server return w } func GetPipeName(s string) string { return strings.TrimPrefix(s, `\\.\pipe`) } func maxBigUint64() *big.Int { n := big.NewInt(0) return n.SetUint64(math.MaxUint64) } func randUint64(t testing.TB) uint64 { n, err := rand.Int(rand.Reader, maxUint64) if err != nil { t.Fail() } return n.Uint64() } func newListener(tb testing.TB) (net.Listener, error) { return winio.ListenPipe(fmt.Sprintf(`\\.\pipe\go-spiffe-test-pipe-%x`, randUint64(tb)), nil) } func getTargetName(addr net.Addr) string { if addr.Network() == "pipe" { // The go-winio library defines the network of a // named pipe address as "pipe", but we use the // "npipe" scheme for named pipes URLs. return "npipe:" + GetPipeName(addr.String()) } return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/test/keys.go000066400000000000000000000017161474173014300240300ustar00rootroot00000000000000package test import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "strings" "testing" "github.com/stretchr/testify/require" ) // Methods to generate private keys. If generation starts slowing down test // execution then switch over to pre-generated keys. // NewEC256Key returns an ECDSA key over the P256 curve func NewEC256Key(tb testing.TB) *ecdsa.PrivateKey { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(tb, err) return key } // NewKeyID returns a random id useful for identifying keys func NewKeyID(tb testing.TB) string { choices := make([]byte, 32) _, err := rand.Read(choices) require.NoError(tb, err) return keyIDFromBytes(choices) } func keyIDFromBytes(choices []byte) string { const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var builder strings.Builder for _, choice := range choices { builder.WriteByte(alphabet[int(choice)%len(alphabet)]) } return builder.String() } golang-github-spiffe-go-spiffe-2.4.0/v2/internal/x509util/000077500000000000000000000000001474173014300231455ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/internal/x509util/util.go000066400000000000000000000023101474173014300244450ustar00rootroot00000000000000package x509util import ( "crypto/x509" ) // NewCertPool returns a new CertPool with the given X.509 certificates func NewCertPool(certs []*x509.Certificate) *x509.CertPool { pool := x509.NewCertPool() for _, cert := range certs { pool.AddCert(cert) } return pool } // CopyX509Authorities copies a slice of X.509 certificates to a new slice. func CopyX509Authorities(x509Authorities []*x509.Certificate) []*x509.Certificate { copiedX509Authorities := make([]*x509.Certificate, len(x509Authorities)) copy(copiedX509Authorities, x509Authorities) return copiedX509Authorities } // CertsEqual returns true if the slices of X.509 certificates are equal. func CertsEqual(a, b []*x509.Certificate) bool { if len(a) != len(b) { return false } for i, cert := range a { if !cert.Equal(b[i]) { return false } } return true } func RawCertsFromCerts(certs []*x509.Certificate) [][]byte { rawCerts := make([][]byte, 0, len(certs)) for _, cert := range certs { rawCerts = append(rawCerts, cert.Raw) } return rawCerts } func ConcatRawCertsFromCerts(certs []*x509.Certificate) []byte { var rawCerts []byte for _, cert := range certs { rawCerts = append(rawCerts, cert.Raw...) } return rawCerts } golang-github-spiffe-go-spiffe-2.4.0/v2/logger/000077500000000000000000000000001474173014300212055ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/logger/logger.go000066400000000000000000000004161474173014300230140ustar00rootroot00000000000000package logger // Logger provides logging facilities to the library. type Logger interface { Debugf(format string, args ...interface{}) Infof(format string, args ...interface{}) Warnf(format string, args ...interface{}) Errorf(format string, args ...interface{}) } golang-github-spiffe-go-spiffe-2.4.0/v2/logger/null.go000066400000000000000000000006621474173014300225120ustar00rootroot00000000000000package logger // Null is a no-op logger. It is used to suppress logging and is the default // logger for the library. var Null Logger = nullLogger{} type nullLogger struct{} func (nullLogger) Debugf(format string, args ...interface{}) {} func (nullLogger) Infof(format string, args ...interface{}) {} func (nullLogger) Warnf(format string, args ...interface{}) {} func (nullLogger) Errorf(format string, args ...interface{}) {} golang-github-spiffe-go-spiffe-2.4.0/v2/logger/std.go000066400000000000000000000011021474173014300223200ustar00rootroot00000000000000package logger import "log" // Std is a logger that uses the Go standard log library. var Std Logger = stdLogger{} type stdLogger struct{} func (stdLogger) Debugf(format string, args ...interface{}) { log.Printf("[DEBUG] "+format+"\n", args...) } func (stdLogger) Infof(format string, args ...interface{}) { log.Printf("[INFO] "+format+"\n", args...) } func (stdLogger) Warnf(format string, args ...interface{}) { log.Printf("[WARN] "+format+"\n", args...) } func (stdLogger) Errorf(format string, args ...interface{}) { log.Printf("[ERROR] "+format+"\n", args...) } golang-github-spiffe-go-spiffe-2.4.0/v2/logger/std_test.go000066400000000000000000000007111474173014300233640ustar00rootroot00000000000000package logger_test import ( "bytes" "log" "testing" "github.com/spiffe/go-spiffe/v2/logger" "github.com/stretchr/testify/require" ) func TestStd(t *testing.T) { buf := new(bytes.Buffer) log.SetOutput(buf) log.SetFlags(0) logger.Std.Debugf("%s", "debug") logger.Std.Warnf("%s", "warn") logger.Std.Infof("%s", "info") logger.Std.Errorf("%s", "error") require.Equal(t, `[DEBUG] debug [WARN] warn [INFO] info [ERROR] error `, buf.String()) } golang-github-spiffe-go-spiffe-2.4.0/v2/logger/writer.go000066400000000000000000000012541474173014300230520ustar00rootroot00000000000000package logger import ( "fmt" "io" ) // Writer provides a logger that outputs logging to the given writer. func Writer(w io.Writer) Logger { return writer{Writer: w} } type writer struct { io.Writer } func (w writer) Debugf(format string, args ...interface{}) { fmt.Fprintf(w.Writer, "[DEBUG] "+format+"\n", args...) } func (w writer) Infof(format string, args ...interface{}) { fmt.Fprintf(w.Writer, "[INFO] "+format+"\n", args...) } func (w writer) Warnf(format string, args ...interface{}) { fmt.Fprintf(w.Writer, "[WARN] "+format+"\n", args...) } func (w writer) Errorf(format string, args ...interface{}) { fmt.Fprintf(w.Writer, "[ERROR] "+format+"\n", args...) } golang-github-spiffe-go-spiffe-2.4.0/v2/logger/writer_test.go000066400000000000000000000006401474173014300241070ustar00rootroot00000000000000package logger_test import ( "bytes" "testing" "github.com/spiffe/go-spiffe/v2/logger" "github.com/stretchr/testify/require" ) func TestWriter(t *testing.T) { buf := new(bytes.Buffer) log := logger.Writer(buf) log.Debugf("%s", "debug") log.Warnf("%s", "warn") log.Infof("%s", "info") log.Errorf("%s", "error") require.Equal(t, `[DEBUG] debug [WARN] warn [INFO] info [ERROR] error `, buf.String()) } golang-github-spiffe-go-spiffe-2.4.0/v2/proto/000077500000000000000000000000001474173014300210715ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/proto/spiffe/000077500000000000000000000000001474173014300223455ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/proto/spiffe/workload/000077500000000000000000000000001474173014300241675ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/proto/spiffe/workload/workload.pb.go000066400000000000000000001102321474173014300267370ustar00rootroot00000000000000// Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 // protoc v3.14.0 // source: workload.proto package workload import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The X509SVIDRequest message conveys parameters for requesting an X.509-SVID. // There are currently no request parameters. type X509SVIDRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *X509SVIDRequest) Reset() { *x = X509SVIDRequest{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *X509SVIDRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*X509SVIDRequest) ProtoMessage() {} func (x *X509SVIDRequest) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use X509SVIDRequest.ProtoReflect.Descriptor instead. func (*X509SVIDRequest) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{0} } // The X509SVIDResponse message carries X.509-SVIDs and related information, // including a set of global CRLs and a list of bundles the workload may use // for federating with foreign trust domains. type X509SVIDResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. A list of X509SVID messages, each of which includes a single // X.509-SVID, its private key, and the bundle for the trust domain. Svids []*X509SVID `protobuf:"bytes,1,rep,name=svids,proto3" json:"svids,omitempty"` // Optional. ASN.1 DER encoded certificate revocation lists. Crl [][]byte `protobuf:"bytes,2,rep,name=crl,proto3" json:"crl,omitempty"` // Optional. CA certificate bundles belonging to foreign trust domains that // the workload should trust, keyed by the SPIFFE ID of the foreign trust // domain. Bundles are ASN.1 DER encoded. FederatedBundles map[string][]byte `protobuf:"bytes,3,rep,name=federated_bundles,json=federatedBundles,proto3" json:"federated_bundles,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *X509SVIDResponse) Reset() { *x = X509SVIDResponse{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *X509SVIDResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*X509SVIDResponse) ProtoMessage() {} func (x *X509SVIDResponse) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use X509SVIDResponse.ProtoReflect.Descriptor instead. func (*X509SVIDResponse) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{1} } func (x *X509SVIDResponse) GetSvids() []*X509SVID { if x != nil { return x.Svids } return nil } func (x *X509SVIDResponse) GetCrl() [][]byte { if x != nil { return x.Crl } return nil } func (x *X509SVIDResponse) GetFederatedBundles() map[string][]byte { if x != nil { return x.FederatedBundles } return nil } // The X509SVID message carries a single SVID and all associated information, // including the X.509 bundle for the trust domain. type X509SVID struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The SPIFFE ID of the SVID in this entry SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` // Required. ASN.1 DER encoded certificate chain. MAY include // intermediates, the leaf certificate (or SVID itself) MUST come first. X509Svid []byte `protobuf:"bytes,2,opt,name=x509_svid,json=x509Svid,proto3" json:"x509_svid,omitempty"` // Required. ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted. X509SvidKey []byte `protobuf:"bytes,3,opt,name=x509_svid_key,json=x509SvidKey,proto3" json:"x509_svid_key,omitempty"` // Required. ASN.1 DER encoded X.509 bundle for the trust domain. Bundle []byte `protobuf:"bytes,4,opt,name=bundle,proto3" json:"bundle,omitempty"` // Optional. An operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. // For example, `internal` and `external` to indicate an SVID for internal or // external use, respectively. Hint string `protobuf:"bytes,5,opt,name=hint,proto3" json:"hint,omitempty"` } func (x *X509SVID) Reset() { *x = X509SVID{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *X509SVID) String() string { return protoimpl.X.MessageStringOf(x) } func (*X509SVID) ProtoMessage() {} func (x *X509SVID) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use X509SVID.ProtoReflect.Descriptor instead. func (*X509SVID) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{2} } func (x *X509SVID) GetSpiffeId() string { if x != nil { return x.SpiffeId } return "" } func (x *X509SVID) GetX509Svid() []byte { if x != nil { return x.X509Svid } return nil } func (x *X509SVID) GetX509SvidKey() []byte { if x != nil { return x.X509SvidKey } return nil } func (x *X509SVID) GetBundle() []byte { if x != nil { return x.Bundle } return nil } func (x *X509SVID) GetHint() string { if x != nil { return x.Hint } return "" } // The X509BundlesRequest message conveys parameters for requesting X.509 // bundles. There are currently no such parameters. type X509BundlesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *X509BundlesRequest) Reset() { *x = X509BundlesRequest{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *X509BundlesRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*X509BundlesRequest) ProtoMessage() {} func (x *X509BundlesRequest) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use X509BundlesRequest.ProtoReflect.Descriptor instead. func (*X509BundlesRequest) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{3} } // The X509BundlesResponse message carries a set of global CRLs and a map of // trust bundles the workload should trust. type X509BundlesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Optional. ASN.1 DER encoded certificate revocation lists. Crl [][]byte `protobuf:"bytes,1,rep,name=crl,proto3" json:"crl,omitempty"` // Required. CA certificate bundles belonging to trust domains that the // workload should trust, keyed by the SPIFFE ID of the trust domain. // Bundles are ASN.1 DER encoded. Bundles map[string][]byte `protobuf:"bytes,2,rep,name=bundles,proto3" json:"bundles,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *X509BundlesResponse) Reset() { *x = X509BundlesResponse{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *X509BundlesResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*X509BundlesResponse) ProtoMessage() {} func (x *X509BundlesResponse) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use X509BundlesResponse.ProtoReflect.Descriptor instead. func (*X509BundlesResponse) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{4} } func (x *X509BundlesResponse) GetCrl() [][]byte { if x != nil { return x.Crl } return nil } func (x *X509BundlesResponse) GetBundles() map[string][]byte { if x != nil { return x.Bundles } return nil } type JWTSVIDRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The audience(s) the workload intends to authenticate against. Audience []string `protobuf:"bytes,1,rep,name=audience,proto3" json:"audience,omitempty"` // Optional. The requested SPIFFE ID for the JWT-SVID. If unset, all // JWT-SVIDs to which the workload is entitled are requested. SpiffeId string `protobuf:"bytes,2,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` } func (x *JWTSVIDRequest) Reset() { *x = JWTSVIDRequest{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *JWTSVIDRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*JWTSVIDRequest) ProtoMessage() {} func (x *JWTSVIDRequest) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use JWTSVIDRequest.ProtoReflect.Descriptor instead. func (*JWTSVIDRequest) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{5} } func (x *JWTSVIDRequest) GetAudience() []string { if x != nil { return x.Audience } return nil } func (x *JWTSVIDRequest) GetSpiffeId() string { if x != nil { return x.SpiffeId } return "" } // The JWTSVIDResponse message conveys JWT-SVIDs. type JWTSVIDResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The list of returned JWT-SVIDs. Svids []*JWTSVID `protobuf:"bytes,1,rep,name=svids,proto3" json:"svids,omitempty"` } func (x *JWTSVIDResponse) Reset() { *x = JWTSVIDResponse{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *JWTSVIDResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*JWTSVIDResponse) ProtoMessage() {} func (x *JWTSVIDResponse) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use JWTSVIDResponse.ProtoReflect.Descriptor instead. func (*JWTSVIDResponse) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{6} } func (x *JWTSVIDResponse) GetSvids() []*JWTSVID { if x != nil { return x.Svids } return nil } // The JWTSVID message carries the JWT-SVID token and associated metadata. type JWTSVID struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The SPIFFE ID of the JWT-SVID. SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` // Required. Encoded JWT using JWS Compact Serialization. Svid string `protobuf:"bytes,2,opt,name=svid,proto3" json:"svid,omitempty"` // Optional. An operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. // For example, `internal` and `external` to indicate an SVID for internal or // external use, respectively. Hint string `protobuf:"bytes,3,opt,name=hint,proto3" json:"hint,omitempty"` } func (x *JWTSVID) Reset() { *x = JWTSVID{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *JWTSVID) String() string { return protoimpl.X.MessageStringOf(x) } func (*JWTSVID) ProtoMessage() {} func (x *JWTSVID) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use JWTSVID.ProtoReflect.Descriptor instead. func (*JWTSVID) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{7} } func (x *JWTSVID) GetSpiffeId() string { if x != nil { return x.SpiffeId } return "" } func (x *JWTSVID) GetSvid() string { if x != nil { return x.Svid } return "" } func (x *JWTSVID) GetHint() string { if x != nil { return x.Hint } return "" } // The JWTBundlesRequest message conveys parameters for requesting JWT bundles. // There are currently no such parameters. type JWTBundlesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *JWTBundlesRequest) Reset() { *x = JWTBundlesRequest{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *JWTBundlesRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*JWTBundlesRequest) ProtoMessage() {} func (x *JWTBundlesRequest) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use JWTBundlesRequest.ProtoReflect.Descriptor instead. func (*JWTBundlesRequest) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{8} } // The JWTBundlesReponse conveys JWT bundles. type JWTBundlesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust // domain. Bundles map[string][]byte `protobuf:"bytes,1,rep,name=bundles,proto3" json:"bundles,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *JWTBundlesResponse) Reset() { *x = JWTBundlesResponse{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *JWTBundlesResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*JWTBundlesResponse) ProtoMessage() {} func (x *JWTBundlesResponse) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use JWTBundlesResponse.ProtoReflect.Descriptor instead. func (*JWTBundlesResponse) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{9} } func (x *JWTBundlesResponse) GetBundles() map[string][]byte { if x != nil { return x.Bundles } return nil } // The ValidateJWTSVIDRequest message conveys request parameters for // JWT-SVID validation. type ValidateJWTSVIDRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The audience of the validating party. The JWT-SVID must // contain an audience claim which contains this value in order to // succesfully validate. Audience string `protobuf:"bytes,1,opt,name=audience,proto3" json:"audience,omitempty"` // Required. The JWT-SVID to validate, encoded using JWS Compact // Serialization. Svid string `protobuf:"bytes,2,opt,name=svid,proto3" json:"svid,omitempty"` } func (x *ValidateJWTSVIDRequest) Reset() { *x = ValidateJWTSVIDRequest{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ValidateJWTSVIDRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidateJWTSVIDRequest) ProtoMessage() {} func (x *ValidateJWTSVIDRequest) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidateJWTSVIDRequest.ProtoReflect.Descriptor instead. func (*ValidateJWTSVIDRequest) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{10} } func (x *ValidateJWTSVIDRequest) GetAudience() string { if x != nil { return x.Audience } return "" } func (x *ValidateJWTSVIDRequest) GetSvid() string { if x != nil { return x.Svid } return "" } // The ValidateJWTSVIDReponse message conveys the JWT-SVID validation results. type ValidateJWTSVIDResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Required. The SPIFFE ID of the validated JWT-SVID. SpiffeId string `protobuf:"bytes,1,opt,name=spiffe_id,json=spiffeId,proto3" json:"spiffe_id,omitempty"` // Optional. Arbitrary claims contained within the payload of the validated // JWT-SVID. Claims *structpb.Struct `protobuf:"bytes,2,opt,name=claims,proto3" json:"claims,omitempty"` } func (x *ValidateJWTSVIDResponse) Reset() { *x = ValidateJWTSVIDResponse{} if protoimpl.UnsafeEnabled { mi := &file_workload_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ValidateJWTSVIDResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ValidateJWTSVIDResponse) ProtoMessage() {} func (x *ValidateJWTSVIDResponse) ProtoReflect() protoreflect.Message { mi := &file_workload_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ValidateJWTSVIDResponse.ProtoReflect.Descriptor instead. func (*ValidateJWTSVIDResponse) Descriptor() ([]byte, []int) { return file_workload_proto_rawDescGZIP(), []int{11} } func (x *ValidateJWTSVIDResponse) GetSpiffeId() string { if x != nil { return x.SpiffeId } return "" } func (x *ValidateJWTSVIDResponse) GetClaims() *structpb.Struct { if x != nil { return x.Claims } return nil } var File_workload_proto protoreflect.FileDescriptor var file_workload_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x11, 0x0a, 0x0f, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe0, 0x01, 0x0a, 0x10, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x05, 0x73, 0x76, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x05, 0x73, 0x76, 0x69, 0x64, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x03, 0x63, 0x72, 0x6c, 0x12, 0x54, 0x0a, 0x11, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x66, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x1a, 0x43, 0x0a, 0x15, 0x46, 0x65, 0x64, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x94, 0x01, 0x0a, 0x08, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x78, 0x35, 0x30, 0x39, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x78, 0x35, 0x30, 0x39, 0x53, 0x76, 0x69, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x78, 0x35, 0x30, 0x39, 0x5f, 0x73, 0x76, 0x69, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x78, 0x35, 0x30, 0x39, 0x53, 0x76, 0x69, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x69, 0x6e, 0x74, 0x22, 0x14, 0x0a, 0x12, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa0, 0x01, 0x0a, 0x13, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x03, 0x63, 0x72, 0x6c, 0x12, 0x3b, 0x0a, 0x07, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x49, 0x0a, 0x0e, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x22, 0x31, 0x0a, 0x0f, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x73, 0x76, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x05, 0x73, 0x76, 0x69, 0x64, 0x73, 0x22, 0x4e, 0x0a, 0x07, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x76, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x76, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x69, 0x6e, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x01, 0x0a, 0x12, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x07, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x48, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x76, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x76, 0x69, 0x64, 0x22, 0x67, 0x0a, 0x17, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x32, 0xc3, 0x02, 0x0a, 0x11, 0x53, 0x70, 0x69, 0x66, 0x66, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x50, 0x49, 0x12, 0x36, 0x0a, 0x0d, 0x46, 0x65, 0x74, 0x63, 0x68, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x12, 0x10, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x3f, 0x0a, 0x10, 0x46, 0x65, 0x74, 0x63, 0x68, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x12, 0x13, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x58, 0x35, 0x30, 0x39, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x0c, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x12, 0x0f, 0x2e, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0f, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x12, 0x12, 0x2e, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x4a, 0x57, 0x54, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x12, 0x17, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x57, 0x54, 0x53, 0x56, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x67, 0x6f, 0x2d, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, 0x69, 0x66, 0x66, 0x65, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x3b, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_workload_proto_rawDescOnce sync.Once file_workload_proto_rawDescData = file_workload_proto_rawDesc ) func file_workload_proto_rawDescGZIP() []byte { file_workload_proto_rawDescOnce.Do(func() { file_workload_proto_rawDescData = protoimpl.X.CompressGZIP(file_workload_proto_rawDescData) }) return file_workload_proto_rawDescData } var file_workload_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_workload_proto_goTypes = []interface{}{ (*X509SVIDRequest)(nil), // 0: X509SVIDRequest (*X509SVIDResponse)(nil), // 1: X509SVIDResponse (*X509SVID)(nil), // 2: X509SVID (*X509BundlesRequest)(nil), // 3: X509BundlesRequest (*X509BundlesResponse)(nil), // 4: X509BundlesResponse (*JWTSVIDRequest)(nil), // 5: JWTSVIDRequest (*JWTSVIDResponse)(nil), // 6: JWTSVIDResponse (*JWTSVID)(nil), // 7: JWTSVID (*JWTBundlesRequest)(nil), // 8: JWTBundlesRequest (*JWTBundlesResponse)(nil), // 9: JWTBundlesResponse (*ValidateJWTSVIDRequest)(nil), // 10: ValidateJWTSVIDRequest (*ValidateJWTSVIDResponse)(nil), // 11: ValidateJWTSVIDResponse nil, // 12: X509SVIDResponse.FederatedBundlesEntry nil, // 13: X509BundlesResponse.BundlesEntry nil, // 14: JWTBundlesResponse.BundlesEntry (*structpb.Struct)(nil), // 15: google.protobuf.Struct } var file_workload_proto_depIdxs = []int32{ 2, // 0: X509SVIDResponse.svids:type_name -> X509SVID 12, // 1: X509SVIDResponse.federated_bundles:type_name -> X509SVIDResponse.FederatedBundlesEntry 13, // 2: X509BundlesResponse.bundles:type_name -> X509BundlesResponse.BundlesEntry 7, // 3: JWTSVIDResponse.svids:type_name -> JWTSVID 14, // 4: JWTBundlesResponse.bundles:type_name -> JWTBundlesResponse.BundlesEntry 15, // 5: ValidateJWTSVIDResponse.claims:type_name -> google.protobuf.Struct 0, // 6: SpiffeWorkloadAPI.FetchX509SVID:input_type -> X509SVIDRequest 3, // 7: SpiffeWorkloadAPI.FetchX509Bundles:input_type -> X509BundlesRequest 5, // 8: SpiffeWorkloadAPI.FetchJWTSVID:input_type -> JWTSVIDRequest 8, // 9: SpiffeWorkloadAPI.FetchJWTBundles:input_type -> JWTBundlesRequest 10, // 10: SpiffeWorkloadAPI.ValidateJWTSVID:input_type -> ValidateJWTSVIDRequest 1, // 11: SpiffeWorkloadAPI.FetchX509SVID:output_type -> X509SVIDResponse 4, // 12: SpiffeWorkloadAPI.FetchX509Bundles:output_type -> X509BundlesResponse 6, // 13: SpiffeWorkloadAPI.FetchJWTSVID:output_type -> JWTSVIDResponse 9, // 14: SpiffeWorkloadAPI.FetchJWTBundles:output_type -> JWTBundlesResponse 11, // 15: SpiffeWorkloadAPI.ValidateJWTSVID:output_type -> ValidateJWTSVIDResponse 11, // [11:16] is the sub-list for method output_type 6, // [6:11] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name } func init() { file_workload_proto_init() } func file_workload_proto_init() { if File_workload_proto != nil { return } if !protoimpl.UnsafeEnabled { file_workload_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*X509SVIDRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*X509SVIDResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*X509SVID); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*X509BundlesRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*X509BundlesResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JWTSVIDRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JWTSVIDResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JWTSVID); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JWTBundlesRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JWTBundlesResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ValidateJWTSVIDRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_workload_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ValidateJWTSVIDResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_workload_proto_rawDesc, NumEnums: 0, NumMessages: 15, NumExtensions: 0, NumServices: 1, }, GoTypes: file_workload_proto_goTypes, DependencyIndexes: file_workload_proto_depIdxs, MessageInfos: file_workload_proto_msgTypes, }.Build() File_workload_proto = out.File file_workload_proto_rawDesc = nil file_workload_proto_goTypes = nil file_workload_proto_depIdxs = nil } golang-github-spiffe-go-spiffe-2.4.0/v2/proto/spiffe/workload/workload.proto000066400000000000000000000146271474173014300271100ustar00rootroot00000000000000syntax = "proto3"; import "google/protobuf/struct.proto"; service SpiffeWorkloadAPI { ///////////////////////////////////////////////////////////////////////// // X509-SVID Profile ///////////////////////////////////////////////////////////////////////// // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, // as well as related information like trust bundles and CRLs. As this // information changes, subsequent messages will be streamed from the // server. rpc FetchX509SVID(X509SVIDRequest) returns (stream X509SVIDResponse); // Fetch trust bundles and CRLs. Useful for clients that only need to // validate SVIDs without obtaining an SVID for themself. As this // information changes, subsequent messages will be streamed from the // server. rpc FetchX509Bundles(X509BundlesRequest) returns (stream X509BundlesResponse); ///////////////////////////////////////////////////////////////////////// // JWT-SVID Profile ///////////////////////////////////////////////////////////////////////// // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, // for the requested audience. If an optional SPIFFE ID is requested, only // the JWT-SVID for that SPIFFE ID is returned. rpc FetchJWTSVID(JWTSVIDRequest) returns (JWTSVIDResponse); // Fetches the JWT bundles, formatted as JWKS documents, keyed by the // SPIFFE ID of the trust domain. As this information changes, subsequent // messages will be streamed from the server. rpc FetchJWTBundles(JWTBundlesRequest) returns (stream JWTBundlesResponse); // Validates a JWT-SVID against the requested audience. Returns the SPIFFE // ID of the JWT-SVID and JWT claims. rpc ValidateJWTSVID(ValidateJWTSVIDRequest) returns (ValidateJWTSVIDResponse); } // The X509SVIDRequest message conveys parameters for requesting an X.509-SVID. // There are currently no request parameters. message X509SVIDRequest { } // The X509SVIDResponse message carries X.509-SVIDs and related information, // including a set of global CRLs and a list of bundles the workload may use // for federating with foreign trust domains. message X509SVIDResponse { // Required. A list of X509SVID messages, each of which includes a single // X.509-SVID, its private key, and the bundle for the trust domain. repeated X509SVID svids = 1; // Optional. ASN.1 DER encoded certificate revocation lists. repeated bytes crl = 2; // Optional. CA certificate bundles belonging to foreign trust domains that // the workload should trust, keyed by the SPIFFE ID of the foreign trust // domain. Bundles are ASN.1 DER encoded. map federated_bundles = 3; } // The X509SVID message carries a single SVID and all associated information, // including the X.509 bundle for the trust domain. message X509SVID { // Required. The SPIFFE ID of the SVID in this entry string spiffe_id = 1; // Required. ASN.1 DER encoded certificate chain. MAY include // intermediates, the leaf certificate (or SVID itself) MUST come first. bytes x509_svid = 2; // Required. ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted. bytes x509_svid_key = 3; // Required. ASN.1 DER encoded X.509 bundle for the trust domain. bytes bundle = 4; // Optional. An operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. // For example, `internal` and `external` to indicate an SVID for internal or // external use, respectively. string hint = 5; } // The X509BundlesRequest message conveys parameters for requesting X.509 // bundles. There are currently no such parameters. message X509BundlesRequest { } // The X509BundlesResponse message carries a set of global CRLs and a map of // trust bundles the workload should trust. message X509BundlesResponse { // Optional. ASN.1 DER encoded certificate revocation lists. repeated bytes crl = 1; // Required. CA certificate bundles belonging to trust domains that the // workload should trust, keyed by the SPIFFE ID of the trust domain. // Bundles are ASN.1 DER encoded. map bundles = 2; } message JWTSVIDRequest { // Required. The audience(s) the workload intends to authenticate against. repeated string audience = 1; // Optional. The requested SPIFFE ID for the JWT-SVID. If unset, all // JWT-SVIDs to which the workload is entitled are requested. string spiffe_id = 2; } // The JWTSVIDResponse message conveys JWT-SVIDs. message JWTSVIDResponse { // Required. The list of returned JWT-SVIDs. repeated JWTSVID svids = 1; } // The JWTSVID message carries the JWT-SVID token and associated metadata. message JWTSVID { // Required. The SPIFFE ID of the JWT-SVID. string spiffe_id = 1; // Required. Encoded JWT using JWS Compact Serialization. string svid = 2; // Optional. An operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. // For example, `internal` and `external` to indicate an SVID for internal or // external use, respectively. string hint = 3; } // The JWTBundlesRequest message conveys parameters for requesting JWT bundles. // There are currently no such parameters. message JWTBundlesRequest { } // The JWTBundlesReponse conveys JWT bundles. message JWTBundlesResponse { // Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust // domain. map bundles = 1; } // The ValidateJWTSVIDRequest message conveys request parameters for // JWT-SVID validation. message ValidateJWTSVIDRequest { // Required. The audience of the validating party. The JWT-SVID must // contain an audience claim which contains this value in order to // succesfully validate. string audience = 1; // Required. The JWT-SVID to validate, encoded using JWS Compact // Serialization. string svid = 2; } // The ValidateJWTSVIDReponse message conveys the JWT-SVID validation results. message ValidateJWTSVIDResponse { // Required. The SPIFFE ID of the validated JWT-SVID. string spiffe_id = 1; // Optional. Arbitrary claims contained within the payload of the validated // JWT-SVID. google.protobuf.Struct claims = 2; } option go_package = "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload;workload"; golang-github-spiffe-go-spiffe-2.4.0/v2/proto/spiffe/workload/workload_grpc.pb.go000066400000000000000000000321071474173014300277560ustar00rootroot00000000000000// Code generated by protoc-gen-go-grpc. DO NOT EDIT. package workload 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. const _ = grpc.SupportPackageIsVersion7 // SpiffeWorkloadAPIClient is the client API for SpiffeWorkloadAPI 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 SpiffeWorkloadAPIClient interface { // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, // as well as related information like trust bundles and CRLs. As this // information changes, subsequent messages will be streamed from the // server. FetchX509SVID(ctx context.Context, in *X509SVIDRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchX509SVIDClient, error) // Fetch trust bundles and CRLs. Useful for clients that only need to // validate SVIDs without obtaining an SVID for themself. As this // information changes, subsequent messages will be streamed from the // server. FetchX509Bundles(ctx context.Context, in *X509BundlesRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchX509BundlesClient, error) // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, // for the requested audience. If an optional SPIFFE ID is requested, only // the JWT-SVID for that SPIFFE ID is returned. FetchJWTSVID(ctx context.Context, in *JWTSVIDRequest, opts ...grpc.CallOption) (*JWTSVIDResponse, error) // Fetches the JWT bundles, formatted as JWKS documents, keyed by the // SPIFFE ID of the trust domain. As this information changes, subsequent // messages will be streamed from the server. FetchJWTBundles(ctx context.Context, in *JWTBundlesRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchJWTBundlesClient, error) // Validates a JWT-SVID against the requested audience. Returns the SPIFFE // ID of the JWT-SVID and JWT claims. ValidateJWTSVID(ctx context.Context, in *ValidateJWTSVIDRequest, opts ...grpc.CallOption) (*ValidateJWTSVIDResponse, error) } type spiffeWorkloadAPIClient struct { cc grpc.ClientConnInterface } func NewSpiffeWorkloadAPIClient(cc grpc.ClientConnInterface) SpiffeWorkloadAPIClient { return &spiffeWorkloadAPIClient{cc} } func (c *spiffeWorkloadAPIClient) FetchX509SVID(ctx context.Context, in *X509SVIDRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchX509SVIDClient, error) { stream, err := c.cc.NewStream(ctx, &_SpiffeWorkloadAPI_serviceDesc.Streams[0], "/SpiffeWorkloadAPI/FetchX509SVID", opts...) if err != nil { return nil, err } x := &spiffeWorkloadAPIFetchX509SVIDClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SpiffeWorkloadAPI_FetchX509SVIDClient interface { Recv() (*X509SVIDResponse, error) grpc.ClientStream } type spiffeWorkloadAPIFetchX509SVIDClient struct { grpc.ClientStream } func (x *spiffeWorkloadAPIFetchX509SVIDClient) Recv() (*X509SVIDResponse, error) { m := new(X509SVIDResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *spiffeWorkloadAPIClient) FetchX509Bundles(ctx context.Context, in *X509BundlesRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchX509BundlesClient, error) { stream, err := c.cc.NewStream(ctx, &_SpiffeWorkloadAPI_serviceDesc.Streams[1], "/SpiffeWorkloadAPI/FetchX509Bundles", opts...) if err != nil { return nil, err } x := &spiffeWorkloadAPIFetchX509BundlesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SpiffeWorkloadAPI_FetchX509BundlesClient interface { Recv() (*X509BundlesResponse, error) grpc.ClientStream } type spiffeWorkloadAPIFetchX509BundlesClient struct { grpc.ClientStream } func (x *spiffeWorkloadAPIFetchX509BundlesClient) Recv() (*X509BundlesResponse, error) { m := new(X509BundlesResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *spiffeWorkloadAPIClient) FetchJWTSVID(ctx context.Context, in *JWTSVIDRequest, opts ...grpc.CallOption) (*JWTSVIDResponse, error) { out := new(JWTSVIDResponse) err := c.cc.Invoke(ctx, "/SpiffeWorkloadAPI/FetchJWTSVID", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *spiffeWorkloadAPIClient) FetchJWTBundles(ctx context.Context, in *JWTBundlesRequest, opts ...grpc.CallOption) (SpiffeWorkloadAPI_FetchJWTBundlesClient, error) { stream, err := c.cc.NewStream(ctx, &_SpiffeWorkloadAPI_serviceDesc.Streams[2], "/SpiffeWorkloadAPI/FetchJWTBundles", opts...) if err != nil { return nil, err } x := &spiffeWorkloadAPIFetchJWTBundlesClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type SpiffeWorkloadAPI_FetchJWTBundlesClient interface { Recv() (*JWTBundlesResponse, error) grpc.ClientStream } type spiffeWorkloadAPIFetchJWTBundlesClient struct { grpc.ClientStream } func (x *spiffeWorkloadAPIFetchJWTBundlesClient) Recv() (*JWTBundlesResponse, error) { m := new(JWTBundlesResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *spiffeWorkloadAPIClient) ValidateJWTSVID(ctx context.Context, in *ValidateJWTSVIDRequest, opts ...grpc.CallOption) (*ValidateJWTSVIDResponse, error) { out := new(ValidateJWTSVIDResponse) err := c.cc.Invoke(ctx, "/SpiffeWorkloadAPI/ValidateJWTSVID", in, out, opts...) if err != nil { return nil, err } return out, nil } // SpiffeWorkloadAPIServer is the server API for SpiffeWorkloadAPI service. // All implementations must embed UnimplementedSpiffeWorkloadAPIServer // for forward compatibility type SpiffeWorkloadAPIServer interface { // Fetch X.509-SVIDs for all SPIFFE identities the workload is entitled to, // as well as related information like trust bundles and CRLs. As this // information changes, subsequent messages will be streamed from the // server. FetchX509SVID(*X509SVIDRequest, SpiffeWorkloadAPI_FetchX509SVIDServer) error // Fetch trust bundles and CRLs. Useful for clients that only need to // validate SVIDs without obtaining an SVID for themself. As this // information changes, subsequent messages will be streamed from the // server. FetchX509Bundles(*X509BundlesRequest, SpiffeWorkloadAPI_FetchX509BundlesServer) error // Fetch JWT-SVIDs for all SPIFFE identities the workload is entitled to, // for the requested audience. If an optional SPIFFE ID is requested, only // the JWT-SVID for that SPIFFE ID is returned. FetchJWTSVID(context.Context, *JWTSVIDRequest) (*JWTSVIDResponse, error) // Fetches the JWT bundles, formatted as JWKS documents, keyed by the // SPIFFE ID of the trust domain. As this information changes, subsequent // messages will be streamed from the server. FetchJWTBundles(*JWTBundlesRequest, SpiffeWorkloadAPI_FetchJWTBundlesServer) error // Validates a JWT-SVID against the requested audience. Returns the SPIFFE // ID of the JWT-SVID and JWT claims. ValidateJWTSVID(context.Context, *ValidateJWTSVIDRequest) (*ValidateJWTSVIDResponse, error) mustEmbedUnimplementedSpiffeWorkloadAPIServer() } // UnimplementedSpiffeWorkloadAPIServer must be embedded to have forward compatible implementations. type UnimplementedSpiffeWorkloadAPIServer struct { } func (UnimplementedSpiffeWorkloadAPIServer) FetchX509SVID(*X509SVIDRequest, SpiffeWorkloadAPI_FetchX509SVIDServer) error { return status.Errorf(codes.Unimplemented, "method FetchX509SVID not implemented") } func (UnimplementedSpiffeWorkloadAPIServer) FetchX509Bundles(*X509BundlesRequest, SpiffeWorkloadAPI_FetchX509BundlesServer) error { return status.Errorf(codes.Unimplemented, "method FetchX509Bundles not implemented") } func (UnimplementedSpiffeWorkloadAPIServer) FetchJWTSVID(context.Context, *JWTSVIDRequest) (*JWTSVIDResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FetchJWTSVID not implemented") } func (UnimplementedSpiffeWorkloadAPIServer) FetchJWTBundles(*JWTBundlesRequest, SpiffeWorkloadAPI_FetchJWTBundlesServer) error { return status.Errorf(codes.Unimplemented, "method FetchJWTBundles not implemented") } func (UnimplementedSpiffeWorkloadAPIServer) ValidateJWTSVID(context.Context, *ValidateJWTSVIDRequest) (*ValidateJWTSVIDResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ValidateJWTSVID not implemented") } func (UnimplementedSpiffeWorkloadAPIServer) mustEmbedUnimplementedSpiffeWorkloadAPIServer() {} // UnsafeSpiffeWorkloadAPIServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to SpiffeWorkloadAPIServer will // result in compilation errors. type UnsafeSpiffeWorkloadAPIServer interface { mustEmbedUnimplementedSpiffeWorkloadAPIServer() } func RegisterSpiffeWorkloadAPIServer(s grpc.ServiceRegistrar, srv SpiffeWorkloadAPIServer) { s.RegisterService(&_SpiffeWorkloadAPI_serviceDesc, srv) } func _SpiffeWorkloadAPI_FetchX509SVID_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(X509SVIDRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SpiffeWorkloadAPIServer).FetchX509SVID(m, &spiffeWorkloadAPIFetchX509SVIDServer{stream}) } type SpiffeWorkloadAPI_FetchX509SVIDServer interface { Send(*X509SVIDResponse) error grpc.ServerStream } type spiffeWorkloadAPIFetchX509SVIDServer struct { grpc.ServerStream } func (x *spiffeWorkloadAPIFetchX509SVIDServer) Send(m *X509SVIDResponse) error { return x.ServerStream.SendMsg(m) } func _SpiffeWorkloadAPI_FetchX509Bundles_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(X509BundlesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SpiffeWorkloadAPIServer).FetchX509Bundles(m, &spiffeWorkloadAPIFetchX509BundlesServer{stream}) } type SpiffeWorkloadAPI_FetchX509BundlesServer interface { Send(*X509BundlesResponse) error grpc.ServerStream } type spiffeWorkloadAPIFetchX509BundlesServer struct { grpc.ServerStream } func (x *spiffeWorkloadAPIFetchX509BundlesServer) Send(m *X509BundlesResponse) error { return x.ServerStream.SendMsg(m) } func _SpiffeWorkloadAPI_FetchJWTSVID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(JWTSVIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpiffeWorkloadAPIServer).FetchJWTSVID(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/SpiffeWorkloadAPI/FetchJWTSVID", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpiffeWorkloadAPIServer).FetchJWTSVID(ctx, req.(*JWTSVIDRequest)) } return interceptor(ctx, in, info, handler) } func _SpiffeWorkloadAPI_FetchJWTBundles_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(JWTBundlesRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(SpiffeWorkloadAPIServer).FetchJWTBundles(m, &spiffeWorkloadAPIFetchJWTBundlesServer{stream}) } type SpiffeWorkloadAPI_FetchJWTBundlesServer interface { Send(*JWTBundlesResponse) error grpc.ServerStream } type spiffeWorkloadAPIFetchJWTBundlesServer struct { grpc.ServerStream } func (x *spiffeWorkloadAPIFetchJWTBundlesServer) Send(m *JWTBundlesResponse) error { return x.ServerStream.SendMsg(m) } func _SpiffeWorkloadAPI_ValidateJWTSVID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ValidateJWTSVIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(SpiffeWorkloadAPIServer).ValidateJWTSVID(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/SpiffeWorkloadAPI/ValidateJWTSVID", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(SpiffeWorkloadAPIServer).ValidateJWTSVID(ctx, req.(*ValidateJWTSVIDRequest)) } return interceptor(ctx, in, info, handler) } var _SpiffeWorkloadAPI_serviceDesc = grpc.ServiceDesc{ ServiceName: "SpiffeWorkloadAPI", HandlerType: (*SpiffeWorkloadAPIServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "FetchJWTSVID", Handler: _SpiffeWorkloadAPI_FetchJWTSVID_Handler, }, { MethodName: "ValidateJWTSVID", Handler: _SpiffeWorkloadAPI_ValidateJWTSVID_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "FetchX509SVID", Handler: _SpiffeWorkloadAPI_FetchX509SVID_Handler, ServerStreams: true, }, { StreamName: "FetchX509Bundles", Handler: _SpiffeWorkloadAPI_FetchX509Bundles_Handler, ServerStreams: true, }, { StreamName: "FetchJWTBundles", Handler: _SpiffeWorkloadAPI_FetchJWTBundles_Handler, ServerStreams: true, }, }, Metadata: "workload.proto", } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffegrpc/000077500000000000000000000000001474173014300220565ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/spiffegrpc/grpccredentials/000077500000000000000000000000001474173014300252275ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/spiffegrpc/grpccredentials/credentials.go000066400000000000000000000124461474173014300300620ustar00rootroot00000000000000package grpccredentials import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" ) // TLSClientCredentials returns TLS credentials which verify and authorize // the server X509-SVID. func TLSClientCredentials(bundle x509bundle.Source, authorizer tlsconfig.Authorizer, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.TLSClientConfig(bundle, authorizer, opts...)), expectPeerID: true} } // MTLSClientCredentials returns TLS credentials which present an X509-SVID // to the server and verifies and authorizes the server X509-SVID. func MTLSClientCredentials(svid x509svid.Source, bundle x509bundle.Source, authorizer tlsconfig.Authorizer, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.MTLSClientConfig(svid, bundle, authorizer, opts...)), expectPeerID: true} } // MTLSWebClientCredentials returns TLS credentials which present an X509-SVID // to the server and verifies the server certificate using provided roots (or // the system roots if nil). func MTLSWebClientCredentials(svid x509svid.Source, roots *x509.CertPool, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.MTLSWebClientConfig(svid, roots, opts...)), expectPeerID: false} } // TLSServerCredentials returns TLS credentials which present an X509-SVID // to the client and does not require or verify client certificates. func TLSServerCredentials(svid x509svid.Source, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.TLSServerConfig(svid, opts...)), expectPeerID: false} } // MTLSServerCredentials returns TLS credentials which present an X509-SVID // to the client and requires, verifies, and authorizes client X509-SVIDs. func MTLSServerCredentials(svid x509svid.Source, bundle x509bundle.Source, authorizer tlsconfig.Authorizer, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.MTLSServerConfig(svid, bundle, authorizer, opts...)), expectPeerID: true} } // MTLSWebServerCredentials returns TLS credentials which present a web // server certificate to the client and requires, verifies, and authorizes // client X509-SVIDs. func MTLSWebServerCredentials(cert *tls.Certificate, bundle x509bundle.Source, authorizer tlsconfig.Authorizer, opts ...tlsconfig.Option) credentials.TransportCredentials { return credentialsWrapper{c: credentials.NewTLS(tlsconfig.MTLSWebServerConfig(cert, bundle, authorizer, opts...)), expectPeerID: true} } // PeerIDFromContext returns the SPIFFE ID from the peer information on the // context. If the peer does not have a SPIFFE ID, or the credentials for the // connection were not provided by this package, the function returns false. func PeerIDFromContext(ctx context.Context) (spiffeid.ID, bool) { p, ok := peer.FromContext(ctx) if !ok { return spiffeid.ID{}, false } return PeerIDFromPeer(p) } // PeerIDFromPeer returns the SPIFFE ID for the peer information on the // context. If the peer does not have a SPIFFE ID, or the credentials for the // connection were not provided by this package, the function returns false. func PeerIDFromPeer(p *peer.Peer) (spiffeid.ID, bool) { authInfo, ok := p.AuthInfo.(authInfoWrapper) if !ok { return spiffeid.ID{}, false } return authInfo.PeerID() } type credentialsWrapper struct { c credentials.TransportCredentials expectPeerID bool } func (w credentialsWrapper) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { return w.wrapHandshake(w.c.ClientHandshake(ctx, authority, rawConn)) } func (w credentialsWrapper) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { return w.wrapHandshake(w.c.ServerHandshake(rawConn)) } func (w credentialsWrapper) wrapHandshake(conn net.Conn, authInfo credentials.AuthInfo, handshakeErr error) (net.Conn, credentials.AuthInfo, error) { if handshakeErr != nil { return nil, nil, handshakeErr } var peerID spiffeid.ID if tlsInfo, ok := authInfo.(credentials.TLSInfo); ok && w.expectPeerID { var err error peerID, err = spiffeid.FromString(tlsInfo.SPIFFEID.String()) if err != nil { conn.Close() return nil, nil, fmt.Errorf("invalid peer SPIFFE ID: %w", err) } } return conn, authInfoWrapper{AuthInfo: authInfo, peerID: peerID}, nil } func (w credentialsWrapper) Info() credentials.ProtocolInfo { return w.c.Info() } func (w credentialsWrapper) Clone() credentials.TransportCredentials { return credentialsWrapper{ c: w.c.Clone(), expectPeerID: w.expectPeerID, } } func (w credentialsWrapper) OverrideServerName(serverName string) error { return w.c.OverrideServerName(serverName) // nolint:staticcheck // wrapper needs to call underlying method until fully deprecated } type authInfoWrapper struct { credentials.AuthInfo peerID spiffeid.ID } func (w authInfoWrapper) PeerID() (spiffeid.ID, bool) { return w.peerID, !w.peerID.IsZero() } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffegrpc/grpccredentials/credentials_test.go000066400000000000000000000131031474173014300311100ustar00rootroot00000000000000package grpccredentials_test import ( "context" "net" "sync" "testing" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/spiffegrpc/grpccredentials" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/examples/helloworld/helloworld" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) func TestCredentials(t *testing.T) { webRoots, webCert := test.CreateWebCredentials(t) td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) bundle := ca.Bundle() serverSVID := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/server")) clientSVID := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/client")) serverID := serverSVID.ID.String() clientID := clientSVID.ID.String() serverMTLS := grpccredentials.MTLSServerCredentials(serverSVID, bundle, tlsconfig.AuthorizeAny()) clientMTLS := grpccredentials.MTLSClientCredentials(clientSVID, bundle, tlsconfig.AuthorizeAny()) serverTLS := grpccredentials.TLSServerCredentials(serverSVID) clientTLS := grpccredentials.TLSClientCredentials(bundle, tlsconfig.AuthorizeAny()) serverWeb := grpccredentials.MTLSWebServerCredentials(webCert, bundle, tlsconfig.AuthorizeAny()) clientWeb := grpccredentials.MTLSWebClientCredentials(clientSVID, webRoots) t.Run("mTLS to mTLS", func(t *testing.T) { // Handshake will succeed. testCredentials(t, clientMTLS, serverMTLS, expectResult{ Code: codes.OK, ServerID: serverID, ClientID: clientID, }) }) t.Run("TLS to mTLS", func(t *testing.T) { // Handshake will fail since server requires client SVID testCredentials(t, clientTLS, serverMTLS, expectResult{ Code: codes.Unavailable, }) }) t.Run("Web to mTLS", func(t *testing.T) { // Handshake will fail because client is doing hostname validation // against a server SVID testCredentials(t, clientWeb, serverMTLS, expectResult{ Code: codes.Unavailable, MessageContains: `cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`, }) }) t.Run("mTLS to TLS", func(t *testing.T) { // Handshake will succeed, but the server won't pick up (or validate) // the client SVID. testCredentials(t, clientMTLS, serverTLS, expectResult{ Code: codes.OK, ServerID: serverID, ClientID: "", }) }) t.Run("TLS to TLS", func(t *testing.T) { // Handshake will succeed, but the server won't pick up (or validate) // the client SVID. testCredentials(t, clientTLS, serverTLS, expectResult{ Code: codes.OK, ServerID: serverID, ClientID: "", }) }) t.Run("Web to TLS", func(t *testing.T) { // Handshake will fail because client is doing hostname validation // against a server SVID testCredentials(t, clientWeb, serverTLS, expectResult{ Code: codes.Unavailable, MessageContains: `cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`, }) }) t.Run("mTLS to Web", func(t *testing.T) { // Handshake will fail because client expects server SVID testCredentials(t, clientMTLS, serverWeb, expectResult{ Code: codes.Unavailable, MessageContains: `certificate contains no URI SAN`, }) }) t.Run("TLS to Web", func(t *testing.T) { // Handshake will fail because client expects server SVID testCredentials(t, clientTLS, serverWeb, expectResult{ Code: codes.Unavailable, MessageContains: `could not get leaf SPIFFE ID: certificate contains no URI SAN`, }) }) t.Run("Web to Web", func(t *testing.T) { // Handshake will succeed, but the server won't pick up (or validate) // the client SVID. testCredentials(t, clientWeb, serverWeb, expectResult{ Code: codes.OK, ServerID: "", // No server SVID for web server ClientID: clientID, }) }) } type expectResult struct { Code codes.Code MessageContains string ClientID string ServerID string } func testCredentials(t *testing.T, clientCreds, serverCreds credentials.TransportCredentials, expect expectResult) { var wg sync.WaitGroup defer wg.Wait() ctx, cancel := context.WithCancel(context.Background()) defer cancel() listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer listener.Close() server := grpc.NewServer(grpc.Creds(serverCreds)) defer server.Stop() helloworld.RegisterGreeterServer(server, greeterServer{}) wg.Add(1) go func() { defer wg.Done() _ = server.Serve(listener) }() conn, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(clientCreds)) require.NoError(t, err) defer conn.Close() clientPeer := new(peer.Peer) var clientID string resp, err := helloworld.NewGreeterClient(conn).SayHello(ctx, &helloworld.HelloRequest{}, grpc.Peer(clientPeer)) if err == nil { clientID = resp.Message } st := status.Convert(err) serverID, serverIDOK := grpccredentials.PeerIDFromPeer(clientPeer) assert.Equal(t, expect.ServerID != "", serverIDOK) assert.Equal(t, expect.Code, st.Code()) assert.Contains(t, st.Message(), expect.MessageContains) assert.Equal(t, expect.ClientID, clientID) assert.Equal(t, expect.ServerID, serverID.String()) } type greeterServer struct { helloworld.UnimplementedGreeterServer } func (s greeterServer) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) { peerID, _ := grpccredentials.PeerIDFromContext(ctx) return &helloworld.HelloReply{Message: peerID.String()}, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/000077500000000000000000000000001474173014300215175ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/charset_backcompat_allow.go000066400000000000000000000011701474173014300270600ustar00rootroot00000000000000//go:build spiffeid_charset_backcompat // +build spiffeid_charset_backcompat package spiffeid func isBackcompatTrustDomainChar(c uint8) bool { if isSubDelim(c) { return true } switch c { // unreserved case '~': return true default: return false } } func isBackcompatPathChar(c uint8) bool { if isSubDelim(c) { return true } switch c { // unreserved case '~': return true // gen-delims case ':', '[', ']', '@': return true default: return false } } func isSubDelim(c uint8) bool { switch c { case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': return true default: return false } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/charset_backcompat_deny.go000066400000000000000000000003361474173014300267040ustar00rootroot00000000000000//go:build !spiffeid_charset_backcompat // +build !spiffeid_charset_backcompat package spiffeid func isBackcompatTrustDomainChar(c uint8) bool { return false } func isBackcompatPathChar(c uint8) bool { return false } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/errors.go000066400000000000000000000014141474173014300233620ustar00rootroot00000000000000package spiffeid import "errors" var ( errBadTrustDomainChar = errors.New("trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") errBadPathSegmentChar = errors.New("path segment characters are limited to letters, numbers, dots, dashes, and underscores") errDotSegment = errors.New("path cannot contain dot segments") errNoLeadingSlash = errors.New("path must have a leading slash") errEmpty = errors.New("cannot be empty") errEmptySegment = errors.New("path cannot contain empty segments") errMissingTrustDomain = errors.New("trust domain is missing") errTrailingSlash = errors.New("path cannot have a trailing slash") errWrongScheme = errors.New("scheme is missing or invalid") ) golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/id.go000066400000000000000000000165641474173014300224560ustar00rootroot00000000000000package spiffeid import ( "errors" "fmt" "net/url" "strings" ) const ( schemePrefix = "spiffe://" schemePrefixLen = len(schemePrefix) ) // FromPath returns a new SPIFFE ID in the given trust domain and with the // given path. The supplied path must be a valid absolute path according to the // SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func FromPath(td TrustDomain, path string) (ID, error) { if err := ValidatePath(path); err != nil { return ID{}, err } return makeID(td, path) } // FromPathf returns a new SPIFFE ID from the formatted path in the given trust // domain. The formatted path must be a valid absolute path according to the // SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func FromPathf(td TrustDomain, format string, args ...interface{}) (ID, error) { path, err := FormatPath(format, args...) if err != nil { return ID{}, err } return makeID(td, path) } // FromSegments returns a new SPIFFE ID in the given trust domain with joined // path segments. The path segments must be valid according to the SPIFFE // specification and must not contain path separators. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func FromSegments(td TrustDomain, segments ...string) (ID, error) { path, err := JoinPathSegments(segments...) if err != nil { return ID{}, err } return makeID(td, path) } // FromString parses a SPIFFE ID from a string. func FromString(id string) (ID, error) { switch { case id == "": return ID{}, errEmpty case !strings.HasPrefix(id, schemePrefix): return ID{}, errWrongScheme } pathidx := schemePrefixLen for ; pathidx < len(id); pathidx++ { c := id[pathidx] if c == '/' { break } if !isValidTrustDomainChar(c) { return ID{}, errBadTrustDomainChar } } if pathidx == schemePrefixLen { return ID{}, errMissingTrustDomain } if err := ValidatePath(id[pathidx:]); err != nil { return ID{}, err } return ID{ id: id, pathidx: pathidx, }, nil } // FromStringf parses a SPIFFE ID from a formatted string. func FromStringf(format string, args ...interface{}) (ID, error) { return FromString(fmt.Sprintf(format, args...)) } // FromURI parses a SPIFFE ID from a URI. func FromURI(uri *url.URL) (ID, error) { return FromString(uri.String()) } // ID is a SPIFFE ID type ID struct { id string // pathidx tracks the index to the beginning of the path inside of id. This // is used when extracting the trust domain or path portions of the id. pathidx int } // TrustDomain returns the trust domain of the SPIFFE ID. func (id ID) TrustDomain() TrustDomain { if id.IsZero() { return TrustDomain{} } return TrustDomain{name: id.id[schemePrefixLen:id.pathidx]} } // MemberOf returns true if the SPIFFE ID is a member of the given trust domain. func (id ID) MemberOf(td TrustDomain) bool { return id.TrustDomain() == td } // Path returns the path of the SPIFFE ID inside the trust domain. func (id ID) Path() string { return id.id[id.pathidx:] } // String returns the string representation of the SPIFFE ID, e.g., // "spiffe://example.org/foo/bar". func (id ID) String() string { return id.id } // URL returns a URL for SPIFFE ID. func (id ID) URL() *url.URL { if id.IsZero() { return &url.URL{} } return &url.URL{ Scheme: "spiffe", Host: id.TrustDomain().String(), Path: id.Path(), } } // IsZero returns true if the SPIFFE ID is the zero value. func (id ID) IsZero() bool { return id.id == "" } // AppendPath returns an ID with the appended path. It will fail if called on a // zero value. The path to append must be a valid absolute path according to // the SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) AppendPath(path string) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot append path on a zero ID value") } if err := ValidatePath(path); err != nil { return ID{}, err } id.id += path return id, nil } // AppendPathf returns an ID with the appended formatted path. It will fail if // called on a zero value. The formatted path must be a valid absolute path // according to the SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) AppendPathf(format string, args ...interface{}) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot append path on a zero ID value") } path, err := FormatPath(format, args...) if err != nil { return ID{}, err } id.id += path return id, nil } // AppendSegments returns an ID with the appended joined path segments. It // will fail if called on a zero value. The path segments must be valid // according to the SPIFFE specification and must not contain path separators. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) AppendSegments(segments ...string) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot append path segments on a zero ID value") } path, err := JoinPathSegments(segments...) if err != nil { return ID{}, err } id.id += path return id, nil } // Replace path returns an ID with the given path in the same trust domain. It // will fail if called on a zero value. The given path must be a valid absolute // path according to the SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) ReplacePath(path string) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot replace path on a zero ID value") } return FromPath(id.TrustDomain(), path) } // ReplacePathf returns an ID with the formatted path in the same trust domain. // It will fail if called on a zero value. The formatted path must be a valid // absolute path according to the SPIFFE specification. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) ReplacePathf(format string, args ...interface{}) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot replace path on a zero ID value") } return FromPathf(id.TrustDomain(), format, args...) } // ReplaceSegments returns an ID with the joined path segments in the same // trust domain. It will fail if called on a zero value. The path segments must // be valid according to the SPIFFE specification and must not contain path // separators. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func (id ID) ReplaceSegments(segments ...string) (ID, error) { if id.IsZero() { return ID{}, errors.New("cannot replace path segments on a zero ID value") } return FromSegments(id.TrustDomain(), segments...) } // MarshalText returns a text representation of the ID. If the ID is the zero // value, nil is returned. func (id ID) MarshalText() ([]byte, error) { if id.IsZero() { return nil, nil } return []byte(id.String()), nil } // UnmarshalText decodes a text representation of the ID. If the text is empty, // the ID is set to the zero value. func (id *ID) UnmarshalText(text []byte) error { if len(text) == 0 { *id = ID{} return nil } unmarshaled, err := FromString(string(text)) if err != nil { return err } *id = unmarshaled return nil } func makeID(td TrustDomain, path string) (ID, error) { if td.IsZero() { return ID{}, errors.New("trust domain is empty") } return ID{ id: schemePrefix + td.name + path, pathidx: schemePrefixLen + len(td.name), }, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/id_test.go000066400000000000000000000414301474173014300235030ustar00rootroot00000000000000package spiffeid_test import ( "encoding/json" "fmt" "net/url" "testing" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( td = spiffeid.RequireTrustDomainFromString("trustdomain") lowerAlpha = asSet( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", ) upperAlpha = asSet( "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ) numbers = asSet( "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ) special = asSet( ".", "-", "_", ) tdChars = mergeSets(lowerAlpha, numbers, special) pathChars = mergeSets(lowerAlpha, upperAlpha, numbers, special) ) func TestFromString(t *testing.T) { assertOK := func(t *testing.T, in string, expectTD spiffeid.TrustDomain, expectPath string) { id, err := spiffeid.FromString(in) if assert.NoError(t, err) { assertIDEqual(t, id, expectTD, expectPath) } id, err = spiffeid.FromStringf("%s", in) if assert.NoError(t, err) { assertIDEqual(t, id, expectTD, expectPath) } assert.NotPanics(t, func() { id = spiffeid.RequireFromString(in) assertIDEqual(t, id, expectTD, expectPath) }) assert.NotPanics(t, func() { id = spiffeid.RequireFromStringf("%s", in) assertIDEqual(t, id, expectTD, expectPath) }) } assertFail := func(t *testing.T, in string, expectErr string) { out, err := spiffeid.FromString(in) assertErrorContains(t, err, expectErr) assert.Zero(t, out) out, err = spiffeid.FromStringf("%s", in) assertErrorContains(t, err, expectErr) assert.Zero(t, out) assert.Panics(t, func() { spiffeid.RequireFromString(in) }) assert.Panics(t, func() { spiffeid.RequireFromStringf("%s", in) }) } t.Run("reject empty", func(t *testing.T) { assertFail(t, "", `cannot be empty`) }) t.Run("path is optional", func(t *testing.T) { assertOK(t, "spiffe://trustdomain", td, "") }) // Go all the way through 255, which ensures we reject UTF-8 appropriately for i := 0; i < 256; i++ { if i == '/' { // Don't test / since it is the delimeter between path segments continue } s := string(rune(i)) suffix := fmt.Sprintf("%X", i) if _, ok := tdChars[s]; ok { t.Run("allow good trustdomain char "+suffix, func(t *testing.T) { assertOK(t, "spiffe://trustdomain"+s+"/path", spiffeid.RequireTrustDomainFromString("trustdomain"+s), "/path") }) } else { t.Run("reject bad trustdomain char "+suffix, func(t *testing.T) { assertFail(t, "spiffe://trustdomain"+s+"/path", "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") }) } if _, ok := pathChars[s]; ok { t.Run("allow good path char "+suffix, func(t *testing.T) { assertOK(t, "spiffe://trustdomain/path"+s, td, "/path"+s) }) } else { t.Run("reject bad path char "+suffix, func(t *testing.T) { assertFail(t, "spiffe://trustdomain/path"+s, "path segment characters are limited to letters, numbers, dots, dashes, and underscores") }) } } t.Run("reject bad scheme", func(t *testing.T) { assertFail(t, "s", "scheme is missing or invalid") assertFail(t, "spiffe:/", "scheme is missing or invalid") assertFail(t, "Spiffe://", "scheme is missing or invalid") }) t.Run("reject missing trust domain", func(t *testing.T) { assertFail(t, "spiffe://", "trust domain is missing") assertFail(t, "spiffe:///", "trust domain is missing") }) t.Run("reject empty segments", func(t *testing.T) { assertFail(t, "spiffe://trustdomain/", "path cannot have a trailing slash") assertFail(t, "spiffe://trustdomain//", "path cannot contain empty segments") assertFail(t, "spiffe://trustdomain//path", "path cannot contain empty segments") assertFail(t, "spiffe://trustdomain/path/", "path cannot have a trailing slash") }) t.Run("reject dot segments", func(t *testing.T) { assertFail(t, "spiffe://trustdomain/.", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/./path", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/path/./other", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/path/..", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/..", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/../path", "path cannot contain dot segments") assertFail(t, "spiffe://trustdomain/path/../other", "path cannot contain dot segments") // The following are ok since the the segments, while containing dots // are not all dots (or are more than two dots) assertOK(t, "spiffe://trustdomain/.path", td, "/.path") assertOK(t, "spiffe://trustdomain/..path", td, "/..path") assertOK(t, "spiffe://trustdomain/...", td, "/...") }) t.Run("reject percent encoding", func(t *testing.T) { // percent-encoded unicode assertFail(t, "spiffe://%F0%9F%A4%AF/path", "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") assertFail(t, "spiffe://trustdomain/%F0%9F%A4%AF", "path segment characters are limited to letters, numbers, dots, dashes, and underscores") // percent-encoded ascii assertFail(t, "spiffe://%62%61%64/path", "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") assertFail(t, "spiffe://trustdomain/%62%61%64", "path segment characters are limited to letters, numbers, dots, dashes, and underscores") }) } func TestFromURI(t *testing.T) { parseURI := func(s string) *url.URL { u, err := url.Parse(s) require.NoError(t, err) return u } assertOK := func(s string) { id, err := spiffeid.FromURI(parseURI(s)) assert.NoError(t, err) assert.Equal(t, spiffeid.RequireFromString(s), id) } assertFail := func(u *url.URL, expectErr string) { id, err := spiffeid.FromURI(u) assertErrorContains(t, err, expectErr) assert.Zero(t, id) } assertOK("spiffe://trustdomain") assertOK("spiffe://trustdomain/path") assertFail(&url.URL{}, `cannot be empty`) assertFail(&url.URL{Scheme: "SPIFFE", Host: "trustdomain"}, `scheme is missing or invalid`) assertFail(parseURI("spiffe://trust$domain"), `trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores`) assertFail(parseURI("spiffe://trustdomain/path$"), `path segment characters are limited to letters, numbers, dots, dashes, and underscores`) } func TestFromSegments(t *testing.T) { assertOK := func(segments []string, expectPath string) { id, err := spiffeid.FromSegments(td, segments...) assert.NoError(t, err) assertIDEqual(t, id, td, expectPath) } assertFail := func(segments []string, expectErr string) { id, err := spiffeid.FromSegments(td, segments...) assertErrorContains(t, err, expectErr) assert.Zero(t, id) } assertOK(nil, "") assertOK([]string{}, "") assertOK([]string{"foo"}, "/foo") assertOK([]string{"foo", "bar"}, "/foo/bar") assertFail([]string{""}, "path cannot contain empty segments") assertFail([]string{"/"}, "path segment characters are limited to letters, numbers, dots, dashes, and underscores") assertFail([]string{"/foo"}, "path segment characters are limited to letters, numbers, dots, dashes, and underscores") assertFail([]string{"$"}, "path segment characters are limited to letters, numbers, dots, dashes, and underscores") } func TestFromPathf(t *testing.T) { id, err := spiffeid.FromPathf(td, "%s", "/foo") if assert.NoError(t, err) { assertIDEqual(t, id, td, "/foo") } id, err = spiffeid.FromPathf(td, "") if assert.NoError(t, err) { assertIDEqual(t, id, td, "") } id, err = spiffeid.FromPathf(td, "%s", "foo") if assert.EqualError(t, err, `path must have a leading slash`) { assert.Zero(t, id) } id, err = spiffeid.FromPathf(td, "/") if assert.EqualError(t, err, `path cannot have a trailing slash`) { assert.Zero(t, id) } } func TestIDMemberOf(t *testing.T) { // Common case id := spiffeid.RequireFromSegments(td, "path", "element") assert.True(t, id.MemberOf(td)) // Empty path id = spiffeid.RequireFromSegments(td) assert.True(t, id.MemberOf(td)) // Is not member of td2 := spiffeid.RequireTrustDomainFromString("domain2.test") id = spiffeid.RequireFromSegments(td2, "path", "element") assert.False(t, id.MemberOf(td)) } func TestIDString(t *testing.T) { id := spiffeid.ID{} assert.Empty(t, id.String()) id = spiffeid.RequireFromString("spiffe://trustdomain") assert.Equal(t, "spiffe://trustdomain", id.String()) id = spiffeid.RequireFromString("spiffe://trustdomain/path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) } func TestIDURL(t *testing.T) { asURL := func(td, path string) *url.URL { return &url.URL{ Scheme: "spiffe", Host: td, Path: path, } } // Common case id := spiffeid.RequireFromSegments(td, "path", "element") assert.Equal(t, asURL("trustdomain", "/path/element"), id.URL()) // Empty path id = spiffeid.RequireFromSegments(td) assert.Equal(t, asURL("trustdomain", ""), id.URL()) // Empty ID id = spiffeid.ID{} assert.Equal(t, &url.URL{}, id.URL()) } func TestIDReplacePath(t *testing.T) { assertOK := func(startWith, replaceWith, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplacePath(replaceWith) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith, replaceWith, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplacePath(replaceWith) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", "/foo", "/foo") assertOK("/path", "/foo", "/foo") assertFail("", "foo", `path must have a leading slash`) assertFail("/path", "/", `path cannot have a trailing slash`) assertFail("/path", "foo", `path must have a leading slash`) id, err := (spiffeid.ID{}).ReplacePath("/") assert.EqualError(t, err, "cannot replace path on a zero ID value") assert.Zero(t, id) } func TestIDReplacePathf(t *testing.T) { assertOK := func(startWith, replaceWith, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplacePathf("%s", replaceWith) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith, replaceWith, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplacePathf("%s", replaceWith) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", "/foo", "/foo") assertOK("/path", "/foo", "/foo") assertFail("", "foo", `path must have a leading slash`) assertFail("/path", "/", `path cannot have a trailing slash`) assertFail("/path", "foo", `path must have a leading slash`) id, err := (spiffeid.ID{}).ReplacePathf("%s", "/") assert.EqualError(t, err, "cannot replace path on a zero ID value") assert.Zero(t, id) } func TestIDReplaceSegments(t *testing.T) { assertOK := func(startWith string, replaceWith []string, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplaceSegments(replaceWith...) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith string, replaceWith []string, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).ReplaceSegments(replaceWith...) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", []string{"foo"}, "/foo") assertOK("/path", []string{"foo"}, "/foo") assertFail("", []string{""}, `path cannot contain empty segments`) assertFail("", []string{"/foo"}, `path segment characters are limited to letters, numbers, dots, dashes, and underscores`) id, err := (spiffeid.ID{}).ReplaceSegments("/") assert.EqualError(t, err, "cannot replace path segments on a zero ID value") assert.Zero(t, id) } func TestIDAppendPath(t *testing.T) { assertOK := func(startWith, replaceWith, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendPath(replaceWith) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith, replaceWith, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendPath(replaceWith) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", "/foo", "/foo") assertOK("/path", "/foo", "/path/foo") assertFail("", "foo", `path must have a leading slash`) assertFail("/path", "/", `path cannot have a trailing slash`) assertFail("/path", "foo", `path must have a leading slash`) id, err := (spiffeid.ID{}).AppendPath("/") assert.EqualError(t, err, "cannot append path on a zero ID value") assert.Zero(t, id) } func TestIDAppendPathf(t *testing.T) { assertOK := func(startWith, replaceWith, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendPathf("%s", replaceWith) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith, replaceWith, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendPathf("%s", replaceWith) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", "/foo", "/foo") assertOK("/path", "/foo", "/path/foo") assertFail("", "foo", `path must have a leading slash`) assertFail("/path", "/", `path cannot have a trailing slash`) assertFail("/path", "foo", `path must have a leading slash`) id, err := (spiffeid.ID{}).AppendPathf("%s", "/") assert.EqualError(t, err, "cannot append path on a zero ID value") assert.Zero(t, id) } func TestIDAppendSegments(t *testing.T) { assertOK := func(startWith string, replaceWith []string, expectPath string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendSegments(replaceWith...) if assert.NoError(t, err) { assertIDEqual(t, id, td, expectPath) } } assertFail := func(startWith string, replaceWith []string, expectErr string) { id, err := spiffeid.RequireFromPath(td, startWith).AppendSegments(replaceWith...) assert.EqualError(t, err, expectErr) assert.Zero(t, id) } assertOK("", []string{"foo"}, "/foo") assertOK("/path", []string{"foo"}, "/path/foo") assertFail("", []string{""}, `path cannot contain empty segments`) assertFail("", []string{"/foo"}, `path segment characters are limited to letters, numbers, dots, dashes, and underscores`) id, err := (spiffeid.ID{}).AppendSegments("/") assert.EqualError(t, err, "cannot append path segments on a zero ID value") assert.Zero(t, id) } func TestIDIsZero(t *testing.T) { assert.True(t, spiffeid.ID{}.IsZero()) assert.False(t, td.ID().IsZero()) } func TestIDTextMarshaler(t *testing.T) { var s struct { ID spiffeid.ID `json:"id"` } marshaled, err := json.Marshal(s) require.NoError(t, err) require.JSONEq(t, `{"id": ""}`, string(marshaled)) s.ID = spiffeid.RequireFromString("spiffe://trustdomain/path") marshaled, err = json.Marshal(s) require.NoError(t, err) require.JSONEq(t, `{"id": "spiffe://trustdomain/path"}`, string(marshaled)) } func TestIDTextUnmarshaler(t *testing.T) { var s struct { ID spiffeid.ID `json:"id"` } err := json.Unmarshal([]byte(`{"id": ""}`), &s) require.NoError(t, err) require.Zero(t, s.ID) err = json.Unmarshal([]byte(`{"id": "BAD"}`), &s) require.EqualError(t, err, "scheme is missing or invalid") require.Zero(t, s.ID) err = json.Unmarshal([]byte(`{"id": "spiffe://trustdomain/path"}`), &s) require.NoError(t, err) require.Equal(t, "spiffe://trustdomain/path", s.ID.String()) } func BenchmarkIDFromString(b *testing.B) { s := "spiffe://trustdomain/path" for n := 0; n < b.N; n++ { escapes(spiffeid.RequireFromString(s).String()) } } func BenchmarkIDFromPath(b *testing.B) { for n := 0; n < b.N; n++ { escapes(spiffeid.RequireFromPath(td, "/path").String()) } } func BenchmarkIDFromPathf(b *testing.B) { for n := 0; n < b.N; n++ { escapes(spiffeid.RequireFromPathf(td, "%s", "/path").String()) } } func BenchmarkIDFromSegments(b *testing.B) { for n := 0; n < b.N; n++ { escapes(spiffeid.RequireFromSegments(td, "path").String()) } } func assertIDEqual(t *testing.T, id spiffeid.ID, expectTD spiffeid.TrustDomain, expectPath string) { assert.Equal(t, expectTD, id.TrustDomain(), "unexpected trust domain") assert.Equal(t, expectPath, id.Path(), "unexpected path") assert.Equal(t, id.String(), expectTD.IDString()+expectPath, "unexpected ID string") assert.Equal(t, id.URL().String(), id.String(), "unexpected URL representation") } func asSet(ss ...string) map[string]struct{} { set := make(map[string]struct{}) for _, s := range ss { set[s] = struct{}{} } return set } func mergeSets(sets ...map[string]struct{}) map[string]struct{} { merged := make(map[string]struct{}) for _, set := range sets { for k, v := range set { merged[k] = v } } return merged } func assertErrorContains(t *testing.T, err error, contains string) { if assert.Error(t, err) { assert.Contains(t, err.Error(), contains) } } var dummy struct { b bool x string } // escapes prevents a string from being stack allocated due to escape analysis func escapes(s string) { if dummy.b { dummy.x = s } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/match.go000066400000000000000000000020471474173014300231450ustar00rootroot00000000000000package spiffeid import "fmt" // Matcher is used to match a SPIFFE ID. type Matcher func(ID) error // MatchAny matches any SPIFFE ID. func MatchAny() Matcher { return Matcher(func(actual ID) error { return nil }) } // MatchID matches a specific SPIFFE ID. func MatchID(expected ID) Matcher { return Matcher(func(actual ID) error { if actual != expected { return fmt.Errorf("unexpected ID %q", actual) } return nil }) } // MatchOneOf matches any SPIFFE ID in the given list of IDs. func MatchOneOf(expected ...ID) Matcher { set := make(map[ID]struct{}) for _, id := range expected { set[id] = struct{}{} } return Matcher(func(actual ID) error { if _, ok := set[actual]; !ok { return fmt.Errorf("unexpected ID %q", actual) } return nil }) } // MatchMemberOf matches any SPIFFE ID in the given trust domain. func MatchMemberOf(expected TrustDomain) Matcher { return Matcher(func(actual ID) error { if !actual.MemberOf(expected) { return fmt.Errorf("unexpected trust domain %q", actual.TrustDomain()) } return nil }) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/match_test.go000066400000000000000000000055351474173014300242110ustar00rootroot00000000000000package spiffeid_test import ( "testing" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" ) var ( zero = spiffeid.ID{} foo = spiffeid.RequireFromString("spiffe://foo.test") fooA = spiffeid.RequireFromString("spiffe://foo.test/A") fooB = spiffeid.RequireFromString("spiffe://foo.test/B") fooC = spiffeid.RequireFromString("spiffe://foo.test/sub/C") barA = spiffeid.RequireFromString("spiffe://bar.test/A") ) func TestMatchAny(t *testing.T) { testMatch(t, spiffeid.MatchAny(), "", "", "", "", "", "", ) } func TestMatchID_AgainstIDWithPath(t *testing.T) { testMatch(t, spiffeid.MatchID(fooA), `unexpected ID ""`, `unexpected ID "spiffe://foo.test"`, ``, `unexpected ID "spiffe://foo.test/B"`, `unexpected ID "spiffe://foo.test/sub/C"`, `unexpected ID "spiffe://bar.test/A"`, ) } func TestMatchID_AgainstIDWithoutPath(t *testing.T) { testMatch(t, spiffeid.MatchID(foo), `unexpected ID ""`, ``, `unexpected ID "spiffe://foo.test/A"`, `unexpected ID "spiffe://foo.test/B"`, `unexpected ID "spiffe://foo.test/sub/C"`, `unexpected ID "spiffe://bar.test/A"`, ) } func TestMatchOneOf_OnAListOfIDs(t *testing.T) { testMatch(t, spiffeid.MatchOneOf(foo, fooB, fooC, barA), `unexpected ID ""`, ``, `unexpected ID "spiffe://foo.test/A"`, ``, ``, ``, ) } func TestMatchOneOf_OnAnEmptyListOfIDs(t *testing.T) { testMatch(t, spiffeid.MatchOneOf(), `unexpected ID ""`, `unexpected ID "spiffe://foo.test"`, `unexpected ID "spiffe://foo.test/A"`, `unexpected ID "spiffe://foo.test/B"`, `unexpected ID "spiffe://foo.test/sub/C"`, `unexpected ID "spiffe://bar.test/A"`, ) } func TestMatchMemberOf_AgainstNonEmptyTrustDomain(t *testing.T) { testMatch(t, spiffeid.MatchMemberOf(foo.TrustDomain()), `unexpected trust domain ""`, ``, ``, ``, ``, `unexpected trust domain "bar.test"`, ) } func TestMatchMemberOf_AgainstEmptyTrustDomain(t *testing.T) { testMatch(t, spiffeid.MatchMemberOf(spiffeid.TrustDomain{}), ``, `unexpected trust domain "foo.test"`, `unexpected trust domain "foo.test"`, `unexpected trust domain "foo.test"`, `unexpected trust domain "foo.test"`, `unexpected trust domain "bar.test"`, ) } func testMatch(t *testing.T, matcher spiffeid.Matcher, zeroErr, fooErr, fooAErr, fooBErr, fooCErr, barAErr string) { test := func(id spiffeid.ID, expectErr string, msgAndArgs ...interface{}) { err := matcher(id) if expectErr != "" { assert.EqualError(t, err, expectErr, msgAndArgs...) } else { assert.NoError(t, err, msgAndArgs...) } } test(zero, zeroErr, "unexpected result for zero ID") test(foo, fooErr, "unexpected result for foo ID") test(fooA, fooAErr, "unexpected result for fooA ID") test(fooB, fooBErr, "unexpected result for fooB ID") test(fooC, fooCErr, "unexpected result for fooC ID") test(barA, barAErr, "unexpected result for fooD ID") } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/path.go000066400000000000000000000047551474173014300230150ustar00rootroot00000000000000package spiffeid import ( "fmt" "strings" ) // FormatPath builds a path by formatting the given formatting string with // the given args (i.e. fmt.Sprintf). The resulting path must be valid or // an error is returned. func FormatPath(format string, args ...interface{}) (string, error) { path := fmt.Sprintf(format, args...) if err := ValidatePath(path); err != nil { return "", err } return path, nil } // JoinPathSegments joins one or more path segments into a slash separated // path. Segments cannot contain slashes. The resulting path must be valid or // an error is returned. If no segments are provided, an empty string is // returned. func JoinPathSegments(segments ...string) (string, error) { var builder strings.Builder for _, segment := range segments { if err := ValidatePathSegment(segment); err != nil { return "", err } builder.WriteByte('/') builder.WriteString(segment) } return builder.String(), nil } // ValidatePath validates that a path string is a conformant path for a SPIFFE // ID. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func ValidatePath(path string) error { switch { case path == "": return nil case path[0] != '/': return errNoLeadingSlash } segmentStart := 0 segmentEnd := 0 for ; segmentEnd < len(path); segmentEnd++ { c := path[segmentEnd] if c == '/' { switch path[segmentStart:segmentEnd] { case "/": return errEmptySegment case "/.", "/..": return errDotSegment } segmentStart = segmentEnd continue } if !isValidPathSegmentChar(c) { return errBadPathSegmentChar } } switch path[segmentStart:segmentEnd] { case "/": return errTrailingSlash case "/.", "/..": return errDotSegment } return nil } // ValidatePathSegment validates that a string is a conformant segment for // inclusion in the path for a SPIFFE ID. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path func ValidatePathSegment(segment string) error { switch segment { case "": return errEmptySegment case ".", "..": return errDotSegment } for i := 0; i < len(segment); i++ { if !isValidPathSegmentChar(segment[i]) { return errBadPathSegmentChar } } return nil } func isValidPathSegmentChar(c uint8) bool { switch { case c >= 'a' && c <= 'z': return true case c >= 'A' && c <= 'Z': return true case c >= '0' && c <= '9': return true case c == '-', c == '.', c == '_': return true case isBackcompatPathChar(c): return true default: return false } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/path_test.go000066400000000000000000000030531474173014300240420ustar00rootroot00000000000000package spiffeid import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestJoinPathSegments(t *testing.T) { assertBad := func(t *testing.T, expectErr error, segments ...string) { _, err := JoinPathSegments(segments...) assert.ErrorIs(t, err, expectErr) } assertOK := func(t *testing.T, expectPath string, segments ...string) { path, err := JoinPathSegments(segments...) if assert.NoError(t, err) { assert.Equal(t, expectPath, path) } } t.Run("empty", func(t *testing.T) { assertBad(t, errEmptySegment, "") }) t.Run("single dot", func(t *testing.T) { assertBad(t, errDotSegment, ".") }) t.Run("double dot", func(t *testing.T) { assertBad(t, errDotSegment, "..") }) t.Run("invalid char", func(t *testing.T) { assertBad(t, errBadPathSegmentChar, "/") }) t.Run("valid segment", func(t *testing.T) { assertOK(t, "/a", "a") }) t.Run("valid segments", func(t *testing.T) { assertOK(t, "/a/b", "a", "b") }) } func TestValidatePathSegment(t *testing.T) { t.Run("empty", func(t *testing.T) { require.ErrorIs(t, ValidatePathSegment(""), errEmptySegment) }) t.Run("single dot", func(t *testing.T) { require.ErrorIs(t, ValidatePathSegment("."), errDotSegment) }) t.Run("double dot", func(t *testing.T) { require.ErrorIs(t, ValidatePathSegment(".."), errDotSegment) }) t.Run("invalid char", func(t *testing.T) { require.ErrorIs(t, ValidatePathSegment("/"), errBadPathSegmentChar) }) t.Run("valid segment", func(t *testing.T) { require.NoError(t, ValidatePathSegment("a")) }) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/require.go000066400000000000000000000065251474173014300235320ustar00rootroot00000000000000package spiffeid import ( "net/url" ) // RequireFromPath is similar to FromPath except that instead of returning an // error on malformed input, it panics. It should only be used when the input // is statically verifiable. func RequireFromPath(td TrustDomain, path string) ID { id, err := FromPath(td, path) panicOnErr(err) return id } // RequireFromPathf is similar to FromPathf except that instead of returning an // error on malformed input, it panics. It should only be used when the input // is statically verifiable. func RequireFromPathf(td TrustDomain, format string, args ...interface{}) ID { id, err := FromPathf(td, format, args...) panicOnErr(err) return id } // RequireFromSegments is similar to FromSegments except that instead of // returning an error on malformed input, it panics. It should only be used // when the input is statically verifiable. func RequireFromSegments(td TrustDomain, segments ...string) ID { id, err := FromSegments(td, segments...) panicOnErr(err) return id } // RequireFromString is similar to FromString except that instead of returning // an error on malformed input, it panics. It should only be used when the // input is statically verifiable. func RequireFromString(s string) ID { id, err := FromString(s) panicOnErr(err) return id } // RequireFromStringf is similar to FromStringf except that instead of // returning an error on malformed input, it panics. It should only be used // when the input is statically verifiable. func RequireFromStringf(format string, args ...interface{}) ID { id, err := FromStringf(format, args...) panicOnErr(err) return id } // RequireFromURI is similar to FromURI except that instead of returning an // error on malformed input, it panics. It should only be used when the input is // statically verifiable. func RequireFromURI(uri *url.URL) ID { id, err := FromURI(uri) panicOnErr(err) return id } // RequireTrustDomainFromString is similar to TrustDomainFromString except that // instead of returning an error on malformed input, it panics. It should only // be used when the input is statically verifiable. func RequireTrustDomainFromString(s string) TrustDomain { td, err := TrustDomainFromString(s) panicOnErr(err) return td } // RequireTrustDomainFromURI is similar to TrustDomainFromURI except that // instead of returning an error on malformed input, it panics. It should only // be used when the input is statically verifiable. func RequireTrustDomainFromURI(uri *url.URL) TrustDomain { td, err := TrustDomainFromURI(uri) panicOnErr(err) return td } // RequireFormatPath builds a path by formatting the given formatting string // with the given args (i.e. fmt.Sprintf). The resulting path must be valid or // the function panics. It should only be used when the input is statically // verifiable. func RequireFormatPath(format string, args ...interface{}) string { path, err := FormatPath(format, args...) panicOnErr(err) return path } // RequireJoinPathSegments joins one or more path segments into a slash separated // path. Segments cannot contain slashes. The resulting path must be valid or // the function panics. It should only be used when the input is statically // verifiable. func RequireJoinPathSegments(segments ...string) string { path, err := JoinPathSegments(segments...) panicOnErr(err) return path } func panicOnErr(err error) { if err != nil { panic(err) } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/require_test.go000066400000000000000000000055721474173014300245720ustar00rootroot00000000000000package spiffeid_test import ( "net/url" "testing" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" ) func TestRequireFromPath(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromPath(td, "/path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromPath(td, "relative") }) } func TestRequireFromPathf(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromPathf(td, "/%s", "path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromPathf(td, "%s", "relative") }) } func TestRequireFromSegments(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromSegments(td, "path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromSegments(td, "/absolute") }) } func TestRequireFromString(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromString("spiffe://trustdomain/path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromString("") }) } func TestRequireFromStringf(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromStringf("spiffe://trustdomain/%s", "path") assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromStringf("%s://trustdomain/path", "sparfe") }) } func TestRequireFromURI(t *testing.T) { assert.NotPanics(t, func() { id := spiffeid.RequireFromURI(&url.URL{Scheme: "spiffe", Host: "trustdomain", Path: "/path"}) assert.Equal(t, "spiffe://trustdomain/path", id.String()) }) assert.Panics(t, func() { spiffeid.RequireFromURI(&url.URL{}) }) } func TestRequireTrustDomainFromString(t *testing.T) { assert.NotPanics(t, func() { td := spiffeid.RequireTrustDomainFromString("spiffe://trustdomain/path") assert.Equal(t, "trustdomain", td.String()) }) assert.Panics(t, func() { spiffeid.RequireTrustDomainFromString("spiffe://TRUSTDOMAIN/path") }) } func TestRequireTrustDomainFromURI(t *testing.T) { assert.NotPanics(t, func() { td := spiffeid.RequireTrustDomainFromURI(&url.URL{Scheme: "spiffe", Host: "trustdomain", Path: "/path"}) assert.Equal(t, "trustdomain", td.String()) }) assert.Panics(t, func() { spiffeid.RequireTrustDomainFromURI(&url.URL{}) }) } func TestRequireFormatPath(t *testing.T) { assert.NotPanics(t, func() { path := spiffeid.RequireFormatPath("/%s", "path") assert.Equal(t, "/path", path) }) assert.Panics(t, func() { spiffeid.RequireFormatPath("%s", "path") }) } func TestRequireJoinPathSegments(t *testing.T) { assert.NotPanics(t, func() { path := spiffeid.RequireJoinPathSegments("path") assert.Equal(t, "/path", path) }) assert.Panics(t, func() { spiffeid.RequireJoinPathSegments("/absolute") }) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/trustdomain.go000066400000000000000000000066001474173014300244210ustar00rootroot00000000000000package spiffeid import ( "net/url" "strings" ) // TrustDomain represents the trust domain portion of a SPIFFE ID (e.g. // example.org). type TrustDomain struct { name string } // TrustDomainFromString returns a new TrustDomain from a string. The string // can either be a trust domain name (e.g. example.org), or a valid SPIFFE ID // URI (e.g. spiffe://example.org), otherwise an error is returned. // See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#21-trust-domain. func TrustDomainFromString(idOrName string) (TrustDomain, error) { switch { case idOrName == "": return TrustDomain{}, errMissingTrustDomain case strings.Contains(idOrName, ":/"): // The ID looks like it has something like a scheme separator, let's // try to parse as an ID. We use :/ instead of :// since the // diagnostics are better for a bad input like spiffe:/trustdomain. id, err := FromString(idOrName) if err != nil { return TrustDomain{}, err } return id.TrustDomain(), nil default: for i := 0; i < len(idOrName); i++ { if !isValidTrustDomainChar(idOrName[i]) { return TrustDomain{}, errBadTrustDomainChar } } return TrustDomain{name: idOrName}, nil } } // TrustDomainFromURI returns a new TrustDomain from a URI. The URI must be a // valid SPIFFE ID (see FromURI) or an error is returned. The trust domain is // extracted from the host field. func TrustDomainFromURI(uri *url.URL) (TrustDomain, error) { id, err := FromURI(uri) if err != nil { return TrustDomain{}, err } return id.TrustDomain(), nil } // Name returns the trust domain name as a string, e.g. example.org. func (td TrustDomain) Name() string { return td.name } // String returns the trust domain name as a string, e.g. example.org. func (td TrustDomain) String() string { return td.name } // ID returns the SPIFFE ID of the trust domain. func (td TrustDomain) ID() ID { if id, err := makeID(td, ""); err == nil { return id } return ID{} } // IDString returns a string representation of the the SPIFFE ID of the trust // domain, e.g. "spiffe://example.org". func (td TrustDomain) IDString() string { return td.ID().String() } // IsZero returns true if the trust domain is the zero value. func (td TrustDomain) IsZero() bool { return td.name == "" } // Compare returns an integer comparing the trust domain to another // lexicographically. The result will be 0 if td==other, -1 if td < other, and // +1 if td > other. func (td TrustDomain) Compare(other TrustDomain) int { return strings.Compare(td.name, other.name) } // MarshalText returns a text representation of the trust domain. If the trust // domain is the zero value, nil is returned. func (td TrustDomain) MarshalText() ([]byte, error) { if td.IsZero() { return nil, nil } return []byte(td.String()), nil } // UnmarshalText decodes a text representation of the trust domain. If the text // is empty, the trust domain is set to the zero value. func (td *TrustDomain) UnmarshalText(text []byte) error { if len(text) == 0 { *td = TrustDomain{} return nil } unmarshaled, err := TrustDomainFromString(string(text)) if err != nil { return err } *td = unmarshaled return nil } func isValidTrustDomainChar(c uint8) bool { switch { case c >= 'a' && c <= 'z': return true case c >= '0' && c <= '9': return true case c == '-', c == '.', c == '_': return true case isBackcompatTrustDomainChar(c): return true default: return false } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffeid/trustdomain_test.go000066400000000000000000000132721474173014300254630ustar00rootroot00000000000000package spiffeid_test import ( "encoding/json" "fmt" "net/url" "testing" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTrustDomainFromString(t *testing.T) { assertOK := func(t *testing.T, in string, expected spiffeid.TrustDomain) { actual, err := spiffeid.TrustDomainFromString(in) if assert.NoError(t, err) { assert.Equal(t, expected, actual) } assert.NotPanics(t, func() { actual = spiffeid.RequireTrustDomainFromString(in) assert.Equal(t, expected, actual) }) } assertFail := func(t *testing.T, in string, expectErr string) { td, err := spiffeid.TrustDomainFromString(in) assertErrorContains(t, err, expectErr) assert.Zero(t, td) assert.Panics(t, func() { spiffeid.RequireTrustDomainFromString(in) }) } t.Run("reject empty", func(t *testing.T) { assertFail(t, "", `trust domain is missing`) }) t.Run("allow id without path", func(t *testing.T) { assertOK(t, "spiffe://trustdomain", td) }) t.Run("allow id with path", func(t *testing.T) { assertOK(t, "spiffe://trustdomain/path", td) }) t.Run("reject bad ids", func(t *testing.T) { // We don't need to test all shapes of bad IDs, just a decent // representation across scheme, trust domain, and path. assertFail(t, "spiffe:/trustdomain/path", "scheme is missing or invalid") assertFail(t, "spiffe://", "trust domain is missing") assertFail(t, "spiffe:///path", "trust domain is missing") assertFail(t, "spiffe://trustdomain/", "path cannot have a trailing slash") assertFail(t, "spiffe://trustdomain/path/", "path cannot have a trailing slash") assertFail(t, "spiffe://%F0%9F%A4%AF/path", "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") assertFail(t, "spiffe://trustdomain/%F0%9F%A4%AF", "path segment characters are limited to letters, numbers, dots, dashes, and underscores") }) // Go all the way through 255, which ensures we reject UTF-8 appropriately for i := 0; i < 256; i++ { s := string(rune(i)) suffix := fmt.Sprintf("%X", i) if _, ok := tdChars[s]; ok { t.Run("allow good trustdomain char "+suffix, func(t *testing.T) { expected := spiffeid.RequireTrustDomainFromString("trustdomain" + s) assertOK(t, "trustdomain"+s, expected) assertOK(t, "spiffe://trustdomain"+s, expected) }) } else { t.Run("reject bad trustdomain char "+suffix, func(t *testing.T) { assertFail(t, "trustdomain"+s, "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") }) } } } func TestTrustDomainFromURI(t *testing.T) { parseURI := func(s string) *url.URL { u, err := url.Parse(s) require.NoError(t, err) return u } assertOK := func(s string) { u := parseURI(s) td, err := spiffeid.TrustDomainFromURI(u) assert.NoError(t, err) assert.Equal(t, spiffeid.RequireTrustDomainFromString(u.Host), td) } assertFail := func(u *url.URL, expectErr string) { _, err := spiffeid.TrustDomainFromURI(u) assertErrorContains(t, err, expectErr) } assertOK("spiffe://trustdomain") assertOK("spiffe://trustdomain/path") assertFail(&url.URL{}, `cannot be empty`) assertFail(&url.URL{Scheme: "SPIFFE", Host: "trustdomain"}, `scheme is missing or invalid`) assertFail(parseURI("spiffe://trust$domain"), `trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores`) assertFail(parseURI("spiffe://trustdomain/path$"), `path segment characters are limited to letters, numbers, dots, dashes, and underscores`) } func TestTrustDomainID(t *testing.T) { assert.Zero(t, (spiffeid.TrustDomain{}).ID()) expected := spiffeid.RequireFromString("spiffe://trustdomain") for _, s := range []string{"trustdomain", "spiffe://trustdomain", "spiffe://trustdomain/path"} { td = spiffeid.RequireTrustDomainFromString(s) assert.Equal(t, expected, td.ID()) } } func TestTrustDomainIDString(t *testing.T) { assert.Empty(t, (spiffeid.TrustDomain{}).IDString()) const expected = "spiffe://trustdomain" for _, s := range []string{"trustdomain", "spiffe://trustdomain", "spiffe://trustdomain/path"} { td = spiffeid.RequireTrustDomainFromString(s) assert.Equal(t, expected, td.IDString()) } } func TestTrustDomainIsZero(t *testing.T) { assert.True(t, spiffeid.TrustDomain{}.IsZero()) assert.False(t, spiffeid.RequireTrustDomainFromString("trustdomain").IsZero()) } func TestTrustDomainCompare(t *testing.T) { a := spiffeid.RequireTrustDomainFromString("a") b := spiffeid.RequireTrustDomainFromString("b") assert.Equal(t, -1, a.Compare(b)) assert.Equal(t, 0, a.Compare(a)) //nolint:gocritic // this comparison is intentional. assert.Equal(t, 1, b.Compare(a)) } func TestTrustDomainTextMarshaler(t *testing.T) { var s struct { TrustDomain spiffeid.TrustDomain `json:"trustDomain"` } marshaled, err := json.Marshal(s) require.NoError(t, err) require.JSONEq(t, `{"trustDomain": ""}`, string(marshaled)) s.TrustDomain = spiffeid.RequireTrustDomainFromString("trustdomain") marshaled, err = json.Marshal(s) require.NoError(t, err) require.JSONEq(t, `{"trustDomain": "trustdomain"}`, string(marshaled)) } func TestTrustDomainTextUnmarshaler(t *testing.T) { var s struct { TrustDomain spiffeid.TrustDomain `json:"trustDomain"` } err := json.Unmarshal([]byte(`{"trustDomain": ""}`), &s) require.NoError(t, err) require.Zero(t, s.TrustDomain) err = json.Unmarshal([]byte(`{"trustDomain": "BAD"}`), &s) require.EqualError(t, err, "trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores") require.Zero(t, s.TrustDomain) err = json.Unmarshal([]byte(`{"trustDomain": "trustdomain"}`), &s) require.NoError(t, err) require.Equal(t, "trustdomain", s.TrustDomain.String()) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/000077500000000000000000000000001474173014300217255ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/dial.go000066400000000000000000000060041474173014300231650ustar00rootroot00000000000000package spiffetls import ( "context" "crypto/tls" "io" "net" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/zeebo/errs" ) // Dial creates an mTLS connection using an X509-SVID obtained from the // Workload API. The server is authenticated using X.509 bundles also obtained // from the Workload API. The server is authorized using the given authorizer. // // This is the same as DialWithMode using the MTLSClient mode. func Dial(ctx context.Context, network, addr string, authorizer tlsconfig.Authorizer, options ...DialOption) (net.Conn, error) { return DialWithMode(ctx, network, addr, MTLSClient(authorizer), options...) } // DialWithMode creates a TLS connection using the specified mode. func DialWithMode(ctx context.Context, network, addr string, mode DialMode, options ...DialOption) (_ net.Conn, err error) { m := mode.get() var sourceCloser io.Closer if !m.sourceUnneeded { source := m.source if source == nil { source, err = workloadapi.NewX509Source(ctx, m.options...) if err != nil { return nil, spiffetlsErr.New("cannot create X.509 source: %w", err) } // Close source if there is a failure after this point defer func() { if err != nil { source.Close() } }() sourceCloser = source } m.bundle = source m.svid = source } opt := &dialConfig{} for _, option := range options { option.apply(opt) } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // MinVersion is also set by the Hook methods, but just in case... if opt.baseTLSConf != nil { tlsConfig = opt.baseTLSConf } switch m.mode { case tlsClientMode: tlsconfig.HookTLSClientConfig(tlsConfig, m.bundle, m.authorizer) case mtlsClientMode: tlsconfig.HookMTLSClientConfig(tlsConfig, m.svid, m.bundle, m.authorizer, opt.tlsOptions...) case mtlsWebClientMode: tlsconfig.HookMTLSWebClientConfig(tlsConfig, m.svid, m.roots, opt.tlsOptions...) default: return nil, spiffetlsErr.New("unknown client mode: %v", m.mode) } var conn *tls.Conn if opt.dialer != nil { conn, err = tls.DialWithDialer(opt.dialer, network, addr, tlsConfig) } else { conn, err = tls.Dial(network, addr, tlsConfig) } if err != nil { return nil, spiffetlsErr.New("unable to dial: %w", err) } return &clientConn{ Conn: conn, sourceCloser: sourceCloser, }, nil } type clientConn struct { *tls.Conn sourceCloser io.Closer } func (c *clientConn) Close() error { var group errs.Group if c.sourceCloser != nil { group.Add(c.sourceCloser.Close()) } if err := c.Conn.Close(); err != nil { group.Add(spiffetlsErr.New("unable to close TLS connection: %w", err)) } return group.Err() } // PeerID returns the peer SPIFFE ID on the connection. The handshake must have // been completed. Note that in Go's TLS stack, the TLS 1.3 handshake may not // complete until the first read from the connection. func (c *clientConn) PeerID() (spiffeid.ID, error) { return PeerIDFromConnectionState(c.Conn.ConnectionState()) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/examples_test.go000066400000000000000000000017371474173014300251410ustar00rootroot00000000000000package spiffetls_test import ( "context" "crypto/tls" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" ) func ExampleListenMTLS() { td := spiffeid.RequireTrustDomainFromString("example.org") listener, err := spiffetls.Listen(context.TODO(), "tcp", ":8443", tlsconfig.AuthorizeMemberOf(td)) if err != nil { // TODO: error handling } defer listener.Close() } func ExampleListenMTLS_customTLSConfigBase() { td := spiffeid.RequireTrustDomainFromString("example.org") baseConfig := &tls.Config{ // TODO: set up custom configuration. Note that the spiffetls package // will override certificate and verification related fields. MinVersion: tls.VersionTLS12, } listener, err := spiffetls.Listen(context.TODO(), "tcp", ":8443", tlsconfig.AuthorizeMemberOf(td), spiffetls.WithListenTLSConfigBase(baseConfig)) if err != nil { // TODO: error handling } defer listener.Close() } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/listen.go000066400000000000000000000112541474173014300235550ustar00rootroot00000000000000package spiffetls import ( "context" "crypto/tls" "io" "net" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/zeebo/errs" ) // Listen creates an mTLS listener accepting connections on the given network // address using net.Listen. The server X509-SVID is obtained via the Workload // API along with X.509 bundles used to verify client X509-SVIDs. The client is // authorized using the given authorizer. // // This function is the same as ListenWithMode using the MTLSServer mode. func Listen(ctx context.Context, network, laddr string, authorizer tlsconfig.Authorizer, options ...ListenOption) (net.Listener, error) { return ListenWithMode(ctx, network, laddr, MTLSServer(authorizer), options...) } // NewListener creates an mTLS listener which accepts connections from an inner // Listener and wraps each connection with tls.Server. The server X509-SVID is // obtained via the Workload API along with X.509 bundles used to verify client // X509-SVIDs. The client is authorized using the given authorizer. // // This function is the same as NewListenerWithMode using the MTLSServer mode. func NewListener(ctx context.Context, inner net.Listener, authorizer tlsconfig.Authorizer, options ...ListenOption) (net.Listener, error) { return NewListenerWithMode(ctx, inner, MTLSServer(authorizer), options...) } // ListenWithMode creates a TLS listener accepting connections on the given // network address using net.Listen. The given mode controls the authentication // and authorization exercised during the TLS handshake. func ListenWithMode(ctx context.Context, network, laddr string, mode ListenMode, options ...ListenOption) (net.Listener, error) { inner, err := net.Listen(network, laddr) if err != nil { return nil, err } l, err := NewListenerWithMode(ctx, inner, mode, options...) if err != nil { inner.Close() return nil, err } return l, nil } // NewListenerWithMode creates a TLS listener which accepts connections from an // inner Listener and wraps each connection with tls.Server. The given mode // controls the authentication and authorization exercised during the TLS // handshake. func NewListenerWithMode(ctx context.Context, inner net.Listener, mode ListenMode, options ...ListenOption) (_ net.Listener, err error) { m := mode.get() var sourceCloser io.Closer if !m.sourceUnneeded { source := m.source if source == nil { source, err = workloadapi.NewX509Source(ctx, m.options...) if err != nil { return nil, spiffetlsErr.New("cannot create X.509 source: %w", err) } // Close source if there is a failure after this point defer func() { if err != nil { source.Close() } }() sourceCloser = source } m.bundle = source m.svid = source } opt := &listenConfig{} for _, option := range options { option.apply(opt) } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // MinVersion is also set by the Hook methods, but just in case.. if opt.baseTLSConf != nil { tlsConfig = opt.baseTLSConf } switch m.mode { case tlsServerMode: tlsconfig.HookTLSServerConfig(tlsConfig, m.svid, opt.tlsOptions...) case mtlsServerMode: tlsconfig.HookMTLSServerConfig(tlsConfig, m.svid, m.bundle, m.authorizer, opt.tlsOptions...) case mtlsWebServerMode: tlsconfig.HookMTLSWebServerConfig(tlsConfig, m.cert, m.bundle, m.authorizer) default: return nil, spiffetlsErr.New("unknown server mode: %v", m.mode) } return &listener{ inner: tls.NewListener(inner, tlsConfig), sourceCloser: sourceCloser, }, nil } type listener struct { inner net.Listener sourceCloser io.Closer } func (l *listener) Accept() (net.Conn, error) { conn, err := l.inner.Accept() if err != nil { return nil, err } tlsConn, ok := conn.(*tls.Conn) if !ok { // This is purely defensive. The TLS listeners return tls.Conn's by contract. conn.Close() return nil, spiffetlsErr.New("unexpected conn type %T returned by TLS listener", conn) } return &serverConn{Conn: tlsConn}, nil } func (l *listener) Addr() net.Addr { return l.inner.Addr() } func (l *listener) Close() error { var group errs.Group if l.sourceCloser != nil { group.Add(l.sourceCloser.Close()) } if err := l.inner.Close(); err != nil { group.Add(spiffetlsErr.New("unable to close TLS connection: %w", err)) } return group.Err() } type serverConn struct { *tls.Conn } // PeerID returns the peer SPIFFE ID on the connection. The handshake must have // been completed. Note that in Go's TLS stack, the TLS 1.3 handshake may not // complete until the first read from the connection. func (c *serverConn) PeerID() (spiffeid.ID, error) { return PeerIDFromConnectionState(c.Conn.ConnectionState()) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/mode.go000066400000000000000000000322311474173014300232010ustar00rootroot00000000000000package spiffetls import ( "crypto/tls" "crypto/x509" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) type clientMode int const ( tlsClientMode clientMode = iota mtlsClientMode mtlsWebClientMode ) type serverMode int const ( tlsServerMode serverMode = iota mtlsServerMode mtlsWebServerMode ) // DialMode is a SPIFFE TLS dialing mode. type DialMode interface { get() *dialMode } type dialMode struct { mode clientMode // sourceUnneeded is true when a X509Source is not required since // raw sources have already been provided, i.e. when the mode comes from // a *WithRawConfig method. sourceUnneeded bool authorizer tlsconfig.Authorizer source *workloadapi.X509Source options []workloadapi.X509SourceOption bundle x509bundle.Source svid x509svid.Source roots *x509.CertPool } type listenMode struct { mode serverMode // sourceUnneeded is true when a X509Source is not required since // raw sources have already been provided, i.e. when the mode comes from // a *WithRawConfig method. sourceUnneeded bool authorizer tlsconfig.Authorizer source *workloadapi.X509Source options []workloadapi.X509SourceOption bundle x509bundle.Source svid x509svid.Source cert *tls.Certificate } func (l *listenMode) get() *listenMode { return l } func (d *dialMode) get() *dialMode { return d } // TLSClient configures the dialing for TLS. The server X509-SVID is // authenticated using X.509 bundles obtained via the Workload API. The // authorizer is used to authorize the server X509-SVID. func TLSClient(authorizer tlsconfig.Authorizer) DialMode { return &dialMode{ mode: tlsClientMode, authorizer: authorizer, } } // TLSClientWithSource configures the dialing for TLS. The server X509-SVID is // authenticated using X.509 bundles obtained via the provided Workload API // X.509 source. The source must remain valid for the lifetime of the // connection. The authorizer is used to authorize the server X509-SVID. func TLSClientWithSource(authorizer tlsconfig.Authorizer, source *workloadapi.X509Source) DialMode { return &dialMode{ mode: tlsClientMode, authorizer: authorizer, source: source, } } // TLSClientWithSourceOptions configures the dialing for TLS. The server // X509-SVID is authenticated using X.509 bundles obtained via a new Workload // API X.509 source created with the provided source options. The authorizer is // used to authorize the server X509-SVID. func TLSClientWithSourceOptions(authorizer tlsconfig.Authorizer, options ...workloadapi.X509SourceOption) DialMode { return &dialMode{ mode: tlsClientMode, authorizer: authorizer, options: options, } } // TLSClientWithRawConfig configures the dialing for TLS. The server X509-SVID is // authenticated using X.509 bundles obtained via the provided X.509 bundle // source. The source must remain valid for the lifetime of the connection. The // authorizer is used to authorize the server X509-SVID. func TLSClientWithRawConfig(authorizer tlsconfig.Authorizer, bundle x509bundle.Source) DialMode { return &dialMode{ mode: tlsClientMode, sourceUnneeded: true, authorizer: authorizer, bundle: bundle, } } // MTLSClient configures the dialing for mutually authenticated TLS (mTLS). The // client X509-SVID and the X.509 bundles used to authenticate the server // X509-SVID are obtained via the Workload API. The authorizer is used to // authorize the server X509-SVID. func MTLSClient(authorizer tlsconfig.Authorizer) DialMode { return &dialMode{ mode: mtlsClientMode, authorizer: authorizer, } } // MTLSClientWithSource configures the dialing for mutually authenticated TLS // (mTLS). The client X509-SVID and the X.509 bundles used to authenticate the // server X509-SVID are obtained via the provided Workload API X.509 source. // The source must remain valid for the lifetime of the connection. The // authorizer is used to authorize the server X509-SVID. func MTLSClientWithSource(authorizer tlsconfig.Authorizer, source *workloadapi.X509Source) DialMode { return &dialMode{ mode: mtlsClientMode, authorizer: authorizer, source: source, } } // MTLSClientWithSourceOptions configures the dialing for mutually // authenticated TLS (mTLS). The client X509-SVID and the X.509 bundles used to // authenticate the server X509-SVID are obtained via a new Workload API X.509 // source created with the provided source options. The authorizer is used to // authorize the server X509-SVID. func MTLSClientWithSourceOptions(authorizer tlsconfig.Authorizer, options ...workloadapi.X509SourceOption) DialMode { return &dialMode{ mode: mtlsClientMode, authorizer: authorizer, options: options, } } // MTLSClientWithRawConfig configures the dialing for mutually authenticated TLS // (mTLS). The client X509-SVID and the X.509 bundles used to authenticate the // server X509-SVID are obtained via the provided X509-SVID and X.509 bundle // sources. The sources must remain valid for the lifetime of the connection. // The authorizer is used to authorize the server X509-SVID. func MTLSClientWithRawConfig(authorizer tlsconfig.Authorizer, svid x509svid.Source, bundle x509bundle.Source) DialMode { return &dialMode{ mode: mtlsClientMode, sourceUnneeded: true, authorizer: authorizer, svid: svid, bundle: bundle, } } // MTLSWebClient configures the dialing for mutually authenticated TLS (mTLS). // The client X509-SVID is obtained via the Workload API. The roots (or the // system roots if nil) are used to authenticate the server certificate. func MTLSWebClient(roots *x509.CertPool) DialMode { return &dialMode{ mode: mtlsWebClientMode, roots: roots, } } // MTLSWebClientWithSource configures the dialing for mutually authenticated // TLS (mTLS). The client X509-SVID is obtained via the provided Workload API // X.509 source. The source must remain valid for the lifetime of the // connection. The roots (or the system roots if nil) are used to authenticate // the server certificate. func MTLSWebClientWithSource(roots *x509.CertPool, source *workloadapi.X509Source) DialMode { return &dialMode{ mode: mtlsWebClientMode, source: source, roots: roots, } } // MTLSWebClientWithSourceOptions configures the dialing for mutually // authenticated TLS (mTLS). The client X509-SVID is obtained via a new // Workload API X.509 source created with the provided source options. The // roots (or the system roots if nil) are used to authenticate the server // certificate. func MTLSWebClientWithSourceOptions(roots *x509.CertPool, options ...workloadapi.X509SourceOption) DialMode { return &dialMode{ mode: mtlsWebClientMode, options: options, roots: roots, } } // MTLSWebClientWithRawConfig configures the dialing for mutually authenticated // TLS (mTLS). The client X509-SVID is obtained via the provided X509-SVID // source. The source must remain valid for the lifetime of the connection. The // roots (or the system roots if nil) are used to authenticate the server // certificate. func MTLSWebClientWithRawConfig(roots *x509.CertPool, svid x509svid.Source) DialMode { return &dialMode{ mode: mtlsWebClientMode, sourceUnneeded: true, svid: svid, roots: roots, } } // ListenMode is a SPIFFE TLS listening mode. type ListenMode interface { get() *listenMode } // TLSServer configures the listener for TLS. The listener presents an // X509-SVID obtained via the Workload API. func TLSServer() ListenMode { return &listenMode{ mode: tlsServerMode, } } // TLSServerWithSource configures the listener for TLS. The listener presents // an X509-SVID obtained via the provided Workload API X.509 source. The source // must remain valid for the lifetime of the listener. func TLSServerWithSource(source *workloadapi.X509Source) ListenMode { return &listenMode{ mode: tlsServerMode, source: source, } } // TLSServerWithSourceOptions configures the listener for TLS. The listener // presents an X509-SVID obtained via a new Workload API X.509 source created // with the provided source options. func TLSServerWithSourceOptions(options ...workloadapi.X509SourceOption) ListenMode { return &listenMode{ mode: tlsServerMode, options: options, } } // TLSServerWithRawConfig configures the listener for TLS. The listener presents // an X509-SVID obtained via the provided X509-SVID source. The source must // remain valid for the lifetime of the listener. func TLSServerWithRawConfig(svid x509svid.Source) ListenMode { return &listenMode{ mode: tlsServerMode, sourceUnneeded: true, svid: svid, } } // MTLSServer configures the listener for mutually authenticated TLS (mTLS). // The listener presents an X509-SVID and authenticates client X509-SVIDs using // X.509 bundles, both obtained via the Workload API. The authorizer is used to // authorize client X509-SVIDs. func MTLSServer(authorizer tlsconfig.Authorizer) ListenMode { return &listenMode{ mode: mtlsServerMode, authorizer: authorizer, } } // MTLSServerWithSource configures the listener for mutually authenticated TLS // (mTLS). The listener presents an X509-SVID and authenticates client // X509-SVIDs using X.509 bundles, both obtained via the provided Workload API // X.509 source. The source must remain valid for the lifetime of the listener. // The authorizer is used to authorize client X509-SVIDs. func MTLSServerWithSource(authorizer tlsconfig.Authorizer, source *workloadapi.X509Source) ListenMode { return &listenMode{ mode: mtlsServerMode, authorizer: authorizer, source: source, } } // MTLSServerWithSourceOptions configures the listener for mutually // authenticated TLS (mTLS). The listener presents an X509-SVID and // authenticates client X509-SVIDs using X.509 bundles, both obtained via a new // Workload API X.509 source created with the provided source options. The // authorizer is used to authorize client X509-SVIDs. func MTLSServerWithSourceOptions(authorizer tlsconfig.Authorizer, options ...workloadapi.X509SourceOption) ListenMode { return &listenMode{ mode: mtlsServerMode, authorizer: authorizer, options: options, } } // MTLSServerWithRawConfig configures the listener for mutually authenticated TLS // (mTLS). The listener presents an X509-SVID and authenticates client // X509-SVIDs using X.509 bundles, both obtained via the provided X509-SVID and // X.509 bundle sources. The sources must remain valid for the lifetime of the // listener. The authorizer is used to authorize client X509-SVIDs. func MTLSServerWithRawConfig(authorizer tlsconfig.Authorizer, svid x509svid.Source, bundle x509bundle.Source) ListenMode { return &listenMode{ mode: mtlsServerMode, sourceUnneeded: true, authorizer: authorizer, svid: svid, bundle: bundle, } } // MTLSWebServer configures the listener for mutually authenticated TLS (mTLS). // The listener presents an X.509 certificate and authenticates client // X509-SVIDs using X.509 bundles obtained via the Workload API. The authorizer // is used to authorize client X509-SVIDs. func MTLSWebServer(authorizer tlsconfig.Authorizer, cert *tls.Certificate) ListenMode { return &listenMode{ mode: mtlsWebServerMode, cert: cert, authorizer: authorizer, } } // MTLSWebServerWithSource configures the listener for mutually authenticated // TLS (mTLS). The listener presents an X.509 certificate and authenticates // client X509-SVIDs using X.509 bundles obtained via the provided Workload API // X.509 source. The source must remain valid for the lifetime of the listener. // The authorizer is used to authorize client X509-SVIDs. func MTLSWebServerWithSource(authorizer tlsconfig.Authorizer, cert *tls.Certificate, source *workloadapi.X509Source) ListenMode { return &listenMode{ mode: mtlsWebServerMode, cert: cert, source: source, authorizer: authorizer, } } // MTLSWebServerWithSourceOptions configures the listener for mutually // authenticated TLS (mTLS). The listener presents an X.509 certificate and // authenticates client X509-SVIDs using X.509 bundles, both obtained via a new // Workload API X.509 source created with the provided source options. The // authorizer is used to authorize client X509-SVIDs. func MTLSWebServerWithSourceOptions(authorizer tlsconfig.Authorizer, cert *tls.Certificate, options ...workloadapi.X509SourceOption) ListenMode { return &listenMode{ mode: mtlsWebServerMode, cert: cert, options: options, authorizer: authorizer, } } // MTLSWebServerWithRawConfig configures the listener for mutually authenticated // TLS (mTLS). The listener presents an X.509 certificate and authenticates // client X509-SVIDs using X.509 bundles, both obtained via the provided X.509 // bundle source. The source must remain valid for the lifetime of the // listener. The authorizer is used to authorize client X509-SVIDs. func MTLSWebServerWithRawConfig(authorizer tlsconfig.Authorizer, cert *tls.Certificate, bundle x509bundle.Source) ListenMode { return &listenMode{ mode: mtlsWebServerMode, sourceUnneeded: true, authorizer: authorizer, cert: cert, bundle: bundle, } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/option.go000066400000000000000000000041101474173014300235600ustar00rootroot00000000000000package spiffetls import ( "crypto/tls" "net" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/zeebo/errs" ) var spiffetlsErr = errs.Class("spiffetls") // DialOption is an option for dialing. Option's are also DialOption's. type DialOption interface { apply(*dialConfig) } type dialOption func(*dialConfig) func (fn dialOption) apply(c *dialConfig) { fn(c) } type dialConfig struct { baseTLSConf *tls.Config dialer *net.Dialer tlsOptions []tlsconfig.Option } type listenOption func(*listenConfig) type listenConfig struct { baseTLSConf *tls.Config tlsOptions []tlsconfig.Option } func (fn listenOption) apply(c *listenConfig) { fn(c) } // WithDialTLSConfigBase provides a base TLS configuration to use. Fields // related to certificates and verification will be overwritten by this package // as necessary to facilitate SPIFFE authentication. func WithDialTLSConfigBase(base *tls.Config) DialOption { return dialOption(func(c *dialConfig) { c.baseTLSConf = base }) } // WithDialTLSOptions provides options to use for the TLS config. func WithDialTLSOptions(opts ...tlsconfig.Option) DialOption { return dialOption(func(c *dialConfig) { c.tlsOptions = opts }) } // WithDialer provides a net dialer to use. If unset, the standard net dialer // will be used. func WithDialer(dialer *net.Dialer) DialOption { return dialOption(func(c *dialConfig) { c.dialer = dialer }) } // ListenOption is an option for listening. Option's are also ListenOption's. type ListenOption interface { apply(*listenConfig) } // WithListenTLSConfigBase provides a base TLS configuration to use. Fields // related to certificates and verification will be overwritten by this package // as necessary to facilitate SPIFFE authentication. func WithListenTLSConfigBase(base *tls.Config) ListenOption { return listenOption(func(c *listenConfig) { c.baseTLSConf = base }) } // WithListenTLSOptions provides options to use when doing Server mTLS. func WithListenTLSOptions(opts ...tlsconfig.Option) ListenOption { return listenOption(func(c *listenConfig) { c.tlsOptions = opts }) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/peerid.go000066400000000000000000000024231474173014300235250ustar00rootroot00000000000000package spiffetls import ( "crypto/tls" "net" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" ) type PeerIDGetter interface { PeerID() (spiffeid.ID, error) } // PeerIDFromConn returns the peer ID from a server or client peer connection. // The handshake must have been completed. Note that in Go's TLS stack, the TLS // 1.3 handshake may not complete until the first read from the connection. func PeerIDFromConn(conn net.Conn) (spiffeid.ID, error) { if getter, ok := conn.(PeerIDGetter); ok { return getter.PeerID() } return spiffeid.ID{}, spiffetlsErr.New("connection does not expose peer ID") } func PeerIDFromConnectionState(state tls.ConnectionState) (spiffeid.ID, error) { // The connection state unfortunately does not have VerifiedChains set // because SPIFFE TLS does custom verification, i.e., Go's TLS stack only // sets VerifiedChains if it is the one to verify the chain of trust. The // SPIFFE ID must be extracted from the peer certificates. if len(state.PeerCertificates) == 0 { return spiffeid.ID{}, spiffetlsErr.New("no peer certificates") } id, err := x509svid.IDFromCert(state.PeerCertificates[0]) if err != nil { return spiffeid.ID{}, spiffetlsErr.New("invalid peer certificate: %w", err) } return id, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/spiffetls_posix_test.go000066400000000000000000000012771474173014300265430ustar00rootroot00000000000000//go:build !windows // +build !windows package spiffetls_test import ( "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" ) func listenAndDialCasesOS() []listenAndDialCase { return []listenAndDialCase{ { name: "Wrong workload API server socket", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), defaultWlAPIAddr: "wrong-socket-path", dialErr: "spiffetls: cannot create X.509 source: workload endpoint socket URI must have a \"tcp\" or \"unix\" scheme", listenErr: "spiffetls: cannot create X.509 source: workload endpoint socket URI must have a \"tcp\" or \"unix\" scheme", }, } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/spiffetls_test.go000066400000000000000000000414221474173014300253150ustar00rootroot00000000000000package spiffetls_test import ( "bufio" "bytes" "context" "crypto/tls" "fmt" "io" "net" "os" "sync" "syscall" "testing" "time" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( defaultListenProtocol = "tcp" defaultListenLAddr = "localhost:0" ) var ( td = spiffeid.RequireTrustDomainFromString("example.org") clientID = spiffeid.RequireFromString("spiffe://example.org/client-workload") serverID = spiffeid.RequireFromString("spiffe://example.org/server-workload") testMsg = "Hello!\n" ) type testEnv struct { ca *test.CA wlAPISourceA *workloadapi.X509Source wlAPISourceB *workloadapi.X509Source wlAPIClientA *workloadapi.Client wlAPIClientB *workloadapi.Client wlAPIServerA *fakeworkloadapi.WorkloadAPI wlAPIServerB *fakeworkloadapi.WorkloadAPI wlCancel context.CancelFunc err error } type listenAndDialCase struct { name string dialMode spiffetls.DialMode dialOption []spiffetls.DialOption listenMode spiffetls.ListenMode listenOption []spiffetls.ListenOption defaultWlAPIAddr string dialErr string listenErr string listenLAddr string listenProtocol string serverConnPeerIDErr string clientConnPeerIDErr string usesExternalDialer bool usesBaseTLSConfig bool } func TestListenAndDial(t *testing.T) { testEnv, cleanup := setupTestEnv(t) defer cleanup() // Create custom SVID and bundle source (not backed by workload API) bundleSource := testEnv.ca.X509Bundle() svidSource := testEnv.ca.CreateX509SVID(clientID) // Create web credentials webCertPool, webCert := test.CreateWebCredentials(t) // Flag used to detect if an external dialer was actually used externalDialerUsed := false // Buffer used to detect if a base TLS config was actually used externalTLSConfBuffer := &bytes.Buffer{} // Test Table tests := []listenAndDialCase{ // Failure Scenarios { name: "No server listening", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), dialErr: "spiffetls: unable to dial: dial tcp: missing address", }, { name: "Invalid server protocol", listenProtocol: "invalid", listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), listenErr: "listen invalid: unknown network invalid", }, { name: "Missing server port", listenProtocol: "tcp", listenLAddr: "invalid", listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), listenErr: "listen tcp: address invalid: missing port in address", }, { name: "Invalid server source", listenProtocol: "tcp", listenErr: "spiffetls: cannot create X.509 source: workload endpoint socket address is not configured", listenMode: spiffetls.TLSServer(), }, // Dial Option / Listen Option Scenarios { name: "TLSClient dials using TLS base config", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), serverConnPeerIDErr: "spiffetls: no peer certificates", defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), usesBaseTLSConfig: true, dialOption: []spiffetls.DialOption{ spiffetls.WithDialTLSConfigBase(&tls.Config{ MinVersion: tls.VersionTLS12, KeyLogWriter: externalTLSConfBuffer, }), }, }, { name: "TLSClient dials using external dialer", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), serverConnPeerIDErr: "spiffetls: no peer certificates", defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), usesExternalDialer: true, dialOption: []spiffetls.DialOption{ spiffetls.WithDialer(&net.Dialer{ Control: func(network, addr string, c syscall.RawConn) error { externalDialerUsed = true return nil }, }), }, }, { name: "TLSServer with ListenOption", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), listenOption: []spiffetls.ListenOption{ spiffetls.WithListenTLSConfigBase(&tls.Config{ MinVersion: tls.VersionTLS12, KeyLogWriter: externalTLSConfBuffer, }), }, serverConnPeerIDErr: "spiffetls: no peer certificates", defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), }, // Defaults Scenarios { name: "TLSClient succeeds", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), serverConnPeerIDErr: "spiffetls: no peer certificates", }, { name: "TLSClient succeeds with Dial", listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), serverConnPeerIDErr: "spiffetls: no peer certificates", defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), }, { name: "MTLSClient succeeds", dialMode: spiffetls.MTLSClient(tlsconfig.AuthorizeID(serverID)), listenMode: spiffetls.MTLSServerWithSource(tlsconfig.AuthorizeID(clientID), testEnv.wlAPISourceA), defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), }, { name: "MTLSWebClient / MTLSWebServer succeeds", dialMode: spiffetls.MTLSWebClient(webCertPool), listenMode: spiffetls.MTLSWebServer(tlsconfig.AuthorizeID(clientID), webCert), defaultWlAPIAddr: testEnv.wlAPIServerB.Addr(), clientConnPeerIDErr: "spiffetls: no URI SANs", }, // *WithSource Scenario { name: "TLSClientWithSource / TLSServerWithSource succeeds", dialMode: spiffetls.TLSClientWithSource(tlsconfig.AuthorizeID(serverID), testEnv.wlAPISourceB), listenMode: spiffetls.TLSServerWithSource(testEnv.wlAPISourceA), serverConnPeerIDErr: "spiffetls: no peer certificates", }, { name: "MTLSClientWithSource / MTLSServerWithSource succeeds", dialMode: spiffetls.MTLSClientWithSource(tlsconfig.AuthorizeID(serverID), testEnv.wlAPISourceB), listenMode: spiffetls.MTLSServerWithSource(tlsconfig.AuthorizeID(clientID), testEnv.wlAPISourceA), }, { name: "MTLSWebClientWithSource / MTLSWebServerWithSource succeeds", dialMode: spiffetls.MTLSWebClientWithSource(webCertPool, testEnv.wlAPISourceB), listenMode: spiffetls.MTLSWebServerWithSource(tlsconfig.AuthorizeID(clientID), webCert, testEnv.wlAPISourceA), clientConnPeerIDErr: "spiffetls: no URI SANs", }, // *WithSourceOptions Scenario { name: "TLSClientWithSourceOptions / TLSServerWithSourceOptions succeeds", dialMode: spiffetls.TLSClientWithSourceOptions(tlsconfig.AuthorizeID(serverID), workloadapi.WithClient(testEnv.wlAPIClientB)), listenMode: spiffetls.TLSServerWithSourceOptions(workloadapi.WithClientOptions(workloadapi.WithAddr(testEnv.wlAPIServerA.Addr()))), serverConnPeerIDErr: "spiffetls: no peer certificates", }, { name: "MTLSClientWithSourceOptions / MTLSServerWithSourceOptions succeeds", dialMode: spiffetls.MTLSClientWithSourceOptions(tlsconfig.AuthorizeID(serverID), workloadapi.WithClient(testEnv.wlAPIClientB)), listenMode: spiffetls.MTLSServerWithSourceOptions(tlsconfig.AuthorizeID(clientID), workloadapi.WithClientOptions(workloadapi.WithAddr(testEnv.wlAPIServerA.Addr()))), }, { name: "MTLSWebClientWithSourceOptions / MTLSWebServerWithSourceOptions succeeds", dialMode: spiffetls.MTLSWebClientWithSourceOptions(webCertPool, workloadapi.WithClient(testEnv.wlAPIClientB)), listenMode: spiffetls.MTLSWebServerWithSourceOptions(tlsconfig.AuthorizeID(clientID), webCert, workloadapi.WithClientOptions(workloadapi.WithAddr(testEnv.wlAPIServerA.Addr()))), clientConnPeerIDErr: "spiffetls: no URI SANs", }, // *WithRawConfig Scenario { name: "TLSClientWithRawConfig / TLSServerWithRawConfig succeeds", dialMode: spiffetls.TLSClientWithRawConfig(tlsconfig.AuthorizeID(serverID), bundleSource), listenMode: spiffetls.TLSServerWithRawConfig(testEnv.wlAPISourceA), serverConnPeerIDErr: "spiffetls: no peer certificates", }, { name: "MTLSClientWithRawConfig / MTLSServerWithRawConfig succeeds", dialMode: spiffetls.MTLSClientWithRawConfig(tlsconfig.AuthorizeID(serverID), svidSource, bundleSource), listenMode: spiffetls.MTLSServerWithRawConfig(tlsconfig.AuthorizeID(clientID), testEnv.wlAPISourceA, bundleSource), }, { name: "MTLSWebClientWithRawConfig / MTLSWebServerWithRawConfig succeeds", dialMode: spiffetls.MTLSWebClientWithRawConfig(webCertPool, svidSource), listenMode: spiffetls.MTLSWebServerWithRawConfig(tlsconfig.AuthorizeID(clientID), webCert, bundleSource), clientConnPeerIDErr: "spiffetls: no URI SANs", }, } tests = append(tests, listenAndDialCasesOS()...) for _, test := range tests { test := test if test.defaultWlAPIAddr != "" { require.NoError(t, os.Setenv("SPIFFE_ENDPOINT_SOCKET", test.defaultWlAPIAddr)) } else { require.NoError(t, os.Unsetenv("SPIFFE_ENDPOINT_SOCKET")) } t.Run(test.name, func(t *testing.T) { // Start listening listenCtx, cancelListenCtx := context.WithTimeout(context.Background(), 10*time.Second) defer cancelListenCtx() var wg sync.WaitGroup var listenAddr string listenDataCh := make(chan string, 1) listenErrCh := make(chan error, 1) if test.listenLAddr == "" { test.listenLAddr = defaultListenLAddr } if test.listenProtocol == "" { test.listenProtocol = defaultListenProtocol } if test.listenMode != nil { listener, err := spiffetls.ListenWithMode(listenCtx, test.listenProtocol, test.listenLAddr, test.listenMode, test.listenOption...) if test.listenErr != "" { require.EqualError(t, err, test.listenErr) return } require.NoError(t, err) require.NotNil(t, listener) defer listener.Close() listenAddr = listener.Addr().String() wg.Add(1) go func() { defer wg.Done() conn, err := listener.Accept() if err != nil { listenErrCh <- err return } defer conn.Close() data, err := bufio.NewReader(conn).ReadString('\n') if err != nil { listenErrCh <- err return } listenDataCh <- data // Test serverConn.PeerID() spiffeID, err := spiffetls.PeerIDFromConn(conn) if test.serverConnPeerIDErr == "" { assert.NoError(t, err) assert.Equal(t, clientID, spiffeID) } else { assert.EqualError(t, err, test.serverConnPeerIDErr) } }() } else { // Test Listen function _, err := spiffetls.Listen(listenCtx, test.listenProtocol, test.listenLAddr, tlsconfig.AuthorizeID(clientID)) if test.listenErr != "" { require.EqualError(t, err, test.listenErr) } } // Start dialing dialCtx, cancelDialCtx := context.WithTimeout(context.Background(), time.Second*10) defer cancelDialCtx() dialConnCh := make(chan net.Conn, 1) dialErrCh := make(chan error, 1) externalDialerUsed = false externalTLSConfBuffer.Reset() wg.Add(1) go func() { defer wg.Done() var conn net.Conn var err error if test.dialMode == nil { // Test Dial function conn, err = spiffetls.Dial(dialCtx, "tcp", listenAddr, tlsconfig.AuthorizeID(serverID)) } else { conn, err = spiffetls.DialWithMode(dialCtx, "tcp", listenAddr, test.dialMode, test.dialOption...) } if len(test.listenOption) > 0 { assert.NotEmpty(t, externalTLSConfBuffer.Len()) } if err != nil { dialErrCh <- err return } dialConnCh <- conn }() // Assertions defer wg.Wait() for { select { case dialConn := <-dialConnCh: require.NotNil(t, dialConn) defer dialConn.Close() if test.usesExternalDialer { require.True(t, externalDialerUsed) } if test.usesBaseTLSConfig { require.NotEmpty(t, externalTLSConfBuffer.Len()) } // Test clientConn.PeerID() spiffeID, err := spiffetls.PeerIDFromConn(dialConn) if test.clientConnPeerIDErr == "" { require.NoError(t, err) require.Equal(t, serverID, spiffeID) } else { require.Error(t, err) } fmt.Fprint(dialConn, testMsg) case data := <-listenDataCh: require.Equal(t, testMsg, data) return case err := <-listenErrCh: t.Fatalf("Listener failed: %v\n", err) case err := <-dialErrCh: if test.dialErr != "" { require.EqualError(t, err, test.dialErr) return } require.NoError(t, err) case err := <-dialCtx.Done(): t.Fatalf("Dial context timed out: %v", err) case err := <-listenCtx.Done(): t.Fatalf("Listen context timed out: %v", err) } } }) } } func TestClose(t *testing.T) { testEnv, cleanup := setupTestEnv(t) defer cleanup() listenCtx, cancelListenCtx := context.WithTimeout(context.Background(), 10*time.Second) defer cancelListenCtx() listener, err := spiffetls.ListenWithMode(listenCtx, defaultListenProtocol, defaultListenLAddr, spiffetls.TLSServerWithSource(testEnv.wlAPISourceA)) require.NoError(t, err) var wg sync.WaitGroup defer wg.Wait() wg.Add(1) go func() { defer wg.Done() conn, err := listener.Accept() require.NoError(t, err) defer conn.Close() _, _ = io.Copy(conn, conn) }() dialCtx, cancelDialCtx := context.WithTimeout(context.Background(), time.Second*10) defer cancelDialCtx() conn, err := spiffetls.DialWithMode(dialCtx, "tcp", listener.Addr().String(), spiffetls.TLSClientWithSource(tlsconfig.AuthorizeID(serverID), testEnv.wlAPISourceB)) require.NoError(t, err) // Test writing data to the connection dataString := "test data" n, err := conn.Write([]byte(dataString)) require.NoError(t, err) require.Equal(t, len(dataString), n) // Close connection require.NoError(t, conn.Close()) // If the connection was really closed, this should fail _, err = conn.Write([]byte(dataString)) require.Error(t, err) // Connection has been closed already, expect error require.Error(t, conn.Close()) // Close listener require.NoError(t, listener.Close()) // If the listener was really closed, this should fail _, err = listener.Accept() require.Error(t, err) // Listener has been closed already, expect error require.Error(t, listener.Close()) } func setWorkloadAPIResponse(ca *test.CA, s *fakeworkloadapi.WorkloadAPI, spiffeID spiffeid.ID) { s.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: []*x509svid.SVID{ca.CreateX509SVID(spiffeID)}, }) } func setupTestEnv(t *testing.T) (*testEnv, func()) { testEnv := &testEnv{} cleanup := func() { if testEnv.wlAPIClientA != nil { testEnv.wlAPIClientA.Close() } if testEnv.wlAPIClientB != nil { testEnv.wlAPIClientB.Close() } if testEnv.wlAPIServerA != nil { testEnv.wlAPIServerA.Stop() } if testEnv.wlAPIServerB != nil { testEnv.wlAPIServerB.Stop() } if testEnv.wlCancel != nil { testEnv.wlCancel() } if testEnv.err != nil { t.Fatal(testEnv.err) } } // Common CA for client and server SVIDs testEnv.ca = test.NewCA(t, td) // Start two fake workload API servers called "A" and "B" // Workload API Server A provides identities to the server workload testEnv.wlAPIServerA = fakeworkloadapi.New(t) setWorkloadAPIResponse(testEnv.ca, testEnv.wlAPIServerA, serverID) // Workload API Server B provides identities to the client workload testEnv.wlAPIServerB = fakeworkloadapi.New(t) setWorkloadAPIResponse(testEnv.ca, testEnv.wlAPIServerB, clientID) // Create custom workload API sources for the server wlCtx, wlCancel := context.WithTimeout(context.Background(), time.Second*5) testEnv.wlCancel = wlCancel testEnv.wlAPIClientA, testEnv.err = workloadapi.New(wlCtx, workloadapi.WithAddr(testEnv.wlAPIServerA.Addr())) if testEnv.err != nil { cleanup() } testEnv.wlAPISourceA, testEnv.err = workloadapi.NewX509Source(wlCtx, workloadapi.WithClient(testEnv.wlAPIClientA)) if testEnv.err != nil { cleanup() } // Create custom workload API sources for the client testEnv.wlAPIClientB, testEnv.err = workloadapi.New(wlCtx, workloadapi.WithAddr(testEnv.wlAPIServerB.Addr())) if testEnv.err != nil { cleanup() } testEnv.wlAPISourceB, testEnv.err = workloadapi.NewX509Source(wlCtx, workloadapi.WithClient(testEnv.wlAPIClientB)) if testEnv.err != nil { cleanup() } return testEnv, cleanup } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/spiffetls_windows_test.go000066400000000000000000000012771474173014300270730ustar00rootroot00000000000000//go:build windows // +build windows package spiffetls_test import ( "github.com/spiffe/go-spiffe/v2/spiffetls" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" ) func listenAndDialCasesOS() []listenAndDialCase { return []listenAndDialCase{ { name: "Wrong workload API server socket", dialMode: spiffetls.TLSClient(tlsconfig.AuthorizeID(serverID)), defaultWlAPIAddr: "wrong-socket-path", dialErr: "spiffetls: cannot create X.509 source: workload endpoint socket URI must have a \"tcp\" or \"npipe\" scheme", listenErr: "spiffetls: cannot create X.509 source: workload endpoint socket URI must have a \"tcp\" or \"npipe\" scheme", }, } } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/000077500000000000000000000000001474173014300237155ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/authorizer.go000066400000000000000000000024561474173014300264470ustar00rootroot00000000000000package tlsconfig import ( "crypto/x509" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Authorizer authorizes an X509-SVID given the SPIFFE ID and the chain // of trust. The certificate chain starts with the X509-SVID certificate back // to an X.509 root for the trust domain. type Authorizer func(id spiffeid.ID, verifiedChains [][]*x509.Certificate) error // AuthorizeAny allows any SPIFFE ID. func AuthorizeAny() Authorizer { return AdaptMatcher(spiffeid.MatchAny()) } // AuthorizeID allows a specific SPIFFE ID. func AuthorizeID(allowed spiffeid.ID) Authorizer { return AdaptMatcher(spiffeid.MatchID(allowed)) } // AuthorizeOneOf allows any SPIFFE ID in the given list of IDs. func AuthorizeOneOf(allowed ...spiffeid.ID) Authorizer { return AdaptMatcher(spiffeid.MatchOneOf(allowed...)) } // AuthorizeMemberOf allows any SPIFFE ID in the given trust domain. func AuthorizeMemberOf(allowed spiffeid.TrustDomain) Authorizer { return AdaptMatcher(spiffeid.MatchMemberOf(allowed)) } // AdaptMatcher adapts any spiffeid.Matcher for use as an Authorizer which // only authorizes the SPIFFE ID but otherwise ignores the verified chains. func AdaptMatcher(matcher spiffeid.Matcher) Authorizer { return Authorizer(func(actual spiffeid.ID, verifiedChains [][]*x509.Certificate) error { return matcher(actual) }) } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/config.go000066400000000000000000000230151474173014300255120ustar00rootroot00000000000000package tlsconfig import ( "crypto/tls" "crypto/x509" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/svid/x509svid" ) // TLSClientConfig returns a TLS configuration which verifies and authorizes // the server X509-SVID. func TLSClientConfig(bundle x509bundle.Source, authorizer Authorizer, opts ...Option) *tls.Config { config := newTLSConfig() HookTLSClientConfig(config, bundle, authorizer, opts...) return config } // HookTLSClientConfig sets up the TLS configuration to verify and authorize // the server X509-SVID. If there is an existing callback set for // VerifyPeerCertificate it will be wrapped by this package and invoked // after SPIFFE authentication has completed. func HookTLSClientConfig(config *tls.Config, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) { resetAuthFields(config) config.InsecureSkipVerify = true config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...) } // A Option changes the defaults used to by mTLS ClientConfig functions. type Option interface { apply(*options) } type option func(*options) func (fn option) apply(o *options) { fn(o) } type options struct { trace Trace } func newOptions(opts []Option) *options { out := &options{} for _, opt := range opts { opt.apply(out) } return out } // WithTrace will use the provided tracing callbacks // when various TLS config functions gets invoked. func WithTrace(trace Trace) Option { return option(func(opts *options) { opts.trace = trace }) } // MTLSClientConfig returns a TLS configuration which presents an X509-SVID // to the server and verifies and authorizes the server X509-SVID. func MTLSClientConfig(svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) *tls.Config { config := newTLSConfig() HookMTLSClientConfig(config, svid, bundle, authorizer, opts...) return config } // HookMTLSClientConfig sets up the TLS configuration to present an X509-SVID // to the server and verify and authorize the server X509-SVID. If there is an // existing callback set for VerifyPeerCertificate it will be wrapped by // this package and invoked after SPIFFE authentication has completed. func HookMTLSClientConfig(config *tls.Config, svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) { resetAuthFields(config) config.GetClientCertificate = GetClientCertificate(svid, opts...) config.InsecureSkipVerify = true config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...) } // MTLSWebClientConfig returns a TLS configuration which presents an X509-SVID // to the server and verifies the server certificate using provided roots (or // the system roots if nil). func MTLSWebClientConfig(svid x509svid.Source, roots *x509.CertPool, opts ...Option) *tls.Config { config := newTLSConfig() HookMTLSWebClientConfig(config, svid, roots, opts...) return config } // HookMTLSWebClientConfig sets up the TLS configuration to present an // X509-SVID to the server and verifies the server certificate using the // provided roots (or the system roots if nil). func HookMTLSWebClientConfig(config *tls.Config, svid x509svid.Source, roots *x509.CertPool, opts ...Option) { resetAuthFields(config) config.GetClientCertificate = GetClientCertificate(svid, opts...) config.RootCAs = roots } // TLSServerConfig returns a TLS configuration which presents an X509-SVID // to the client and does not require or verify client certificates. func TLSServerConfig(svid x509svid.Source, opts ...Option) *tls.Config { config := newTLSConfig() HookTLSServerConfig(config, svid, opts...) return config } // HookTLSServerConfig sets up the TLS configuration to present an X509-SVID // to the client and to not require or verify client certificates. func HookTLSServerConfig(config *tls.Config, svid x509svid.Source, opts ...Option) { resetAuthFields(config) config.GetCertificate = GetCertificate(svid, opts...) } // MTLSServerConfig returns a TLS configuration which presents an X509-SVID // to the client and requires, verifies, and authorizes client X509-SVIDs. func MTLSServerConfig(svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) *tls.Config { config := newTLSConfig() HookMTLSServerConfig(config, svid, bundle, authorizer, opts...) return config } // HookMTLSServerConfig sets up the TLS configuration to present an X509-SVID // to the client and require, verify, and authorize the client X509-SVID. If // there is an existing callback set for VerifyPeerCertificate it will be // wrapped by this package and invoked after SPIFFE authentication has // completed. func HookMTLSServerConfig(config *tls.Config, svid x509svid.Source, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) { resetAuthFields(config) config.ClientAuth = tls.RequireAnyClientCert config.GetCertificate = GetCertificate(svid, opts...) config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...) } // MTLSWebServerConfig returns a TLS configuration which presents a web // server certificate to the client and requires, verifies, and authorizes // client X509-SVIDs. func MTLSWebServerConfig(cert *tls.Certificate, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) *tls.Config { config := newTLSConfig() HookMTLSWebServerConfig(config, cert, bundle, authorizer, opts...) return config } // HookMTLSWebServerConfig sets up the TLS configuration to presents a web // server certificate to the client and require, verify, and authorize client // X509-SVIDs. If there is an existing callback set for VerifyPeerCertificate // it will be wrapped by this package and invoked after SPIFFE // authentication has completed. func HookMTLSWebServerConfig(config *tls.Config, cert *tls.Certificate, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) { resetAuthFields(config) config.ClientAuth = tls.RequireAnyClientCert config.Certificates = []tls.Certificate{*cert} config.VerifyPeerCertificate = WrapVerifyPeerCertificate(config.VerifyPeerCertificate, bundle, authorizer, opts...) } // GetCertificate returns a GetCertificate callback for tls.Config. It uses the // given X509-SVID getter to obtain a server X509-SVID for the TLS handshake. func GetCertificate(svid x509svid.Source, opts ...Option) func(*tls.ClientHelloInfo) (*tls.Certificate, error) { opt := newOptions(opts) return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return getTLSCertificate(svid, opt.trace) } } // GetClientCertificate returns a GetClientCertificate callback for tls.Config. // It uses the given X509-SVID getter to obtain a client X509-SVID for the TLS // handshake. func GetClientCertificate(svid x509svid.Source, opts ...Option) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { opt := newOptions(opts) return func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return getTLSCertificate(svid, opt.trace) } } // VerifyPeerCertificate returns a VerifyPeerCertificate callback for // tls.Config. It uses the given bundle source and authorizer to verify and // authorize X509-SVIDs provided by peers during the TLS handshake. func VerifyPeerCertificate(bundle x509bundle.Source, authorizer Authorizer, opts ...Option) func([][]byte, [][]*x509.Certificate) error { return func(raw [][]byte, _ [][]*x509.Certificate) error { id, certs, err := x509svid.ParseAndVerify(raw, bundle) if err != nil { return err } return authorizer(id, certs) } } // WrapVerifyPeerCertificate wraps a VerifyPeerCertificate callback, performing // SPIFFE authentication against the peer certificates using the given bundle and // authorizer. The wrapped callback will be passed the verified chains. // Note: TLS clients must set `InsecureSkipVerify` when doing SPIFFE authentication to disable hostname verification. func WrapVerifyPeerCertificate(wrapped func([][]byte, [][]*x509.Certificate) error, bundle x509bundle.Source, authorizer Authorizer, opts ...Option) func([][]byte, [][]*x509.Certificate) error { if wrapped == nil { return VerifyPeerCertificate(bundle, authorizer, opts...) } return func(raw [][]byte, _ [][]*x509.Certificate) error { id, certs, err := x509svid.ParseAndVerify(raw, bundle) if err != nil { return err } if err := authorizer(id, certs); err != nil { return err } return wrapped(raw, certs) } } func getTLSCertificate(svid x509svid.Source, trace Trace) (*tls.Certificate, error) { var traceVal interface{} if trace.GetCertificate != nil { traceVal = trace.GetCertificate(GetCertificateInfo{}) } s, err := svid.GetX509SVID() if err != nil { if trace.GotCertificate != nil { trace.GotCertificate(GotCertificateInfo{Err: err}, traceVal) } return nil, err } cert := &tls.Certificate{ Certificate: make([][]byte, 0, len(s.Certificates)), PrivateKey: s.PrivateKey, } for _, svidCert := range s.Certificates { cert.Certificate = append(cert.Certificate, svidCert.Raw) } if trace.GotCertificate != nil { trace.GotCertificate(GotCertificateInfo{Cert: cert}, traceVal) } return cert, nil } func newTLSConfig() *tls.Config { return &tls.Config{ MinVersion: tls.VersionTLS12, } } func resetAuthFields(config *tls.Config) { if config.MinVersion < tls.VersionTLS12 { config.MinVersion = tls.VersionTLS12 } config.Certificates = nil config.ClientAuth = tls.NoClientCert config.GetCertificate = nil config.GetClientCertificate = nil config.InsecureSkipVerify = false config.NameToCertificate = nil //nolint:staticcheck // setting to nil is OK config.RootCAs = nil } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/config_test.go000066400000000000000000000706501474173014300265600ustar00rootroot00000000000000package tlsconfig_test import ( "bytes" "crypto/tls" "crypto/x509" "errors" "fmt" "strings" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var localTrace = tlsconfig.Trace{ GetCertificate: func(tlsconfig.GetCertificateInfo) interface{} { fmt.Printf("got start of GetTLSCertificate\n") return nil }, GotCertificate: func(tlsconfig.GotCertificateInfo, interface{}) { fmt.Printf("got end of GetTLSCertificate\n") }, } func TestTLSClientConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) config := tlsconfig.TLSClientConfig(bundle, tlsconfig.AuthorizeAny()) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.True(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) } func TestHookTLSClientConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) base := createBaseTLSConfig() config := createTestTLSConfig(base) tlsconfig.HookTLSClientConfig(config, bundle, tlsconfig.AuthorizeAny()) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.True(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func TestMTLSClientConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) svid := &x509svid.SVID{} config := tlsconfig.MTLSClientConfig(svid, bundle, tlsconfig.AuthorizeAny(), tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.NotNil(t, config.GetClientCertificate) assert.True(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) } func TestHookMTLSClientConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) svid := &x509svid.SVID{} base := createBaseTLSConfig() config := createTestTLSConfig(base) tlsconfig.HookMTLSClientConfig(config, svid, bundle, tlsconfig.AuthorizeAny(), tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.NotNil(t, config.GetClientCertificate) assert.True(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func TestMTLSWebClientConfig(t *testing.T) { svid := &x509svid.SVID{} roots := x509.NewCertPool() config := tlsconfig.MTLSWebClientConfig(svid, roots, tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.NotNil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Equal(t, roots, config.RootCAs) assert.Nil(t, config.VerifyPeerCertificate) } func TestHookMTLSWebClientConfig(t *testing.T) { svid := &x509svid.SVID{} base := createBaseTLSConfig() config := createTestTLSConfig(base) roots := x509.NewCertPool() tlsconfig.HookMTLSWebClientConfig(config, svid, roots, tlsconfig.WithTrace(localTrace), ) // Expected AuthFields assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.NotNil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Equal(t, roots, config.RootCAs) assert.Nil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func TestTLSServerConfig(t *testing.T) { svid := &x509svid.SVID{} config := tlsconfig.TLSServerConfig(svid, tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.NotNil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.Nil(t, config.VerifyPeerCertificate) } func TestHookTLSServerConfig(t *testing.T) { svid := &x509svid.SVID{} base := createBaseTLSConfig() config := createTestTLSConfig(base) tlsconfig.HookTLSServerConfig(config, svid, tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.NoClientCert, config.ClientAuth) assert.NotNil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.Nil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func TestMTLSServerConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) svid := &x509svid.SVID{} config := tlsconfig.MTLSServerConfig(svid, bundle, tlsconfig.AuthorizeAny(), tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.RequireAnyClientCert, config.ClientAuth) assert.NotNil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) } func TestHookMTLSServerConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) svid := &x509svid.SVID{} base := createBaseTLSConfig() config := createTestTLSConfig(base) tlsconfig.HookMTLSServerConfig(config, svid, bundle, tlsconfig.AuthorizeAny(), tlsconfig.WithTrace(localTrace), ) assert.Nil(t, config.Certificates) assert.Equal(t, tls.RequireAnyClientCert, config.ClientAuth) assert.NotNil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func TestMTLSWebServerConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) tlsCert := &tls.Certificate{Certificate: [][]byte{[]byte("body")}} config := tlsconfig.MTLSWebServerConfig(tlsCert, bundle, tlsconfig.AuthorizeAny()) assert.Equal(t, []tls.Certificate{*tlsCert}, config.Certificates) assert.Equal(t, tls.RequireAnyClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) } func TestHookMTLSWebServerConfig(t *testing.T) { trustDomain := spiffeid.RequireTrustDomainFromString("trustdomain") bundle := x509bundle.New(trustDomain) tlsCert := &tls.Certificate{Certificate: [][]byte{[]byte("body")}} base := createBaseTLSConfig() config := createTestTLSConfig(base) tlsconfig.HookMTLSWebServerConfig(config, tlsCert, bundle, tlsconfig.AuthorizeAny()) assert.Equal(t, []tls.Certificate{*tlsCert}, config.Certificates) assert.Equal(t, tls.RequireAnyClientCert, config.ClientAuth) assert.Nil(t, config.GetCertificate) assert.Nil(t, config.GetClientCertificate) assert.False(t, config.InsecureSkipVerify) assert.Nil(t, config.NameToCertificate) //nolint:staticcheck // setting to nil is OK assert.Nil(t, config.RootCAs) assert.NotNil(t, config.VerifyPeerCertificate) assertUnrelatedFieldsUntouched(t, base, config) } func hookedTracer(onGetCertificate, onGotCertificate func()) tlsconfig.Trace { return tlsconfig.Trace{ GetCertificate: func(tlsconfig.GetCertificateInfo) interface{} { if onGetCertificate != nil { onGetCertificate() } return nil }, GotCertificate: func(tlsconfig.GotCertificateInfo, interface{}) { if onGotCertificate != nil { onGotCertificate() } }, } } func TestGetCertificate(t *testing.T) { testCases := []struct { name string source *fakeSource err string expectedCerts [][]byte }{ { name: "success", source: &fakeSource{ err: nil, svid: &x509svid.SVID{ ID: spiffeid.RequireFromString("spiffe://trustdomain/host"), Certificates: []*x509.Certificate{ {Raw: []byte("body")}, }, }, }, expectedCerts: [][]byte{[]byte("body")}, }, { name: "source return error", source: &fakeSource{ err: errors.New("some error"), }, err: "some error", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { getCertificateCalls := 0 tracer := hookedTracer( func() { getCertificateCalls++ }, nil, ) getCertificate := tlsconfig.GetCertificate(testCase.source, tlsconfig.WithTrace(tracer)) require.NotNil(t, getCertificate) tlsCert, err := getCertificate(&tls.ClientHelloInfo{}) if testCase.err != "" { require.EqualError(t, err, testCase.err) require.Nil(t, tlsCert) return } require.NoError(t, err) require.Equal(t, testCase.expectedCerts, tlsCert.Certificate) require.Equal(t, 1, getCertificateCalls) }) } } func TestGetClientCertificate(t *testing.T) { testCases := []struct { name string source *fakeSource err string expectedCerts [][]byte }{ { name: "success", source: &fakeSource{ err: nil, svid: &x509svid.SVID{ ID: spiffeid.RequireFromString("spiffe://trustdomain/host"), Certificates: []*x509.Certificate{ {Raw: []byte("body")}, }, }, }, expectedCerts: [][]byte{[]byte("body")}, }, { name: "source return error", source: &fakeSource{ err: errors.New("some error"), }, err: "some error", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { getCertificateCalls := 0 tracer := hookedTracer( func() { getCertificateCalls++ }, nil, ) getClientCertificate := tlsconfig.GetClientCertificate(testCase.source, tlsconfig.WithTrace(tracer)) require.NotNil(t, getClientCertificate) tlsCert, err := getClientCertificate(&tls.CertificateRequestInfo{}) if testCase.err != "" { require.EqualError(t, err, testCase.err) require.Nil(t, tlsCert) return } require.NoError(t, err) require.Equal(t, testCase.expectedCerts, tlsCert.Certificate) require.Equal(t, 1, getCertificateCalls) }) } } func TestVerifyPeerCertificate(t *testing.T) { td := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td) bundle1 := ca1.X509Bundle() svid1 := ca1.CreateX509SVID(spiffeid.RequireFromPath(td, "/host")) svid1Raw := x509util.RawCertsFromCerts(svid1.Certificates) td2 := spiffeid.RequireTrustDomainFromString("domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() testCases := []struct { name string authorizer tlsconfig.Authorizer bundle x509bundle.Source err string raw [][]byte }{ { name: "success", authorizer: tlsconfig.AuthorizeAny(), bundle: bundle1, raw: svid1Raw, }, { name: "parse and validation fails", authorizer: tlsconfig.AuthorizeAny(), bundle: bundle2, err: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, raw: svid1Raw, }, { name: "authorizer fails", authorizer: tlsconfig.AuthorizeMemberOf(td2), bundle: bundle1, err: `unexpected trust domain "domain1.test"`, raw: svid1Raw, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { verifyPeerCertificate := tlsconfig.VerifyPeerCertificate(testCase.bundle, testCase.authorizer) require.NotNil(t, verifyPeerCertificate) err := verifyPeerCertificate(testCase.raw, [][]*x509.Certificate{}) if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) }) } } func TestWrapVerifyPeerCertificate(t *testing.T) { td := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td) bundle1 := ca1.X509Bundle() svid1 := ca1.CreateX509SVID(spiffeid.RequireFromPath(td, "/host")) svid1Raw := x509util.RawCertsFromCerts(svid1.Certificates) td2 := spiffeid.RequireTrustDomainFromString("domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() wrapped := func([][]byte, [][]*x509.Certificate) error { return errors.New("wrapped called") } testCases := []struct { name string authorizer tlsconfig.Authorizer bundle x509bundle.Source err string raw [][]byte wrapped func([][]byte, [][]*x509.Certificate) error }{ { name: "no wrapped", authorizer: tlsconfig.AuthorizeAny(), bundle: bundle1, raw: svid1Raw, }, { name: "parse and validation fails", authorizer: tlsconfig.AuthorizeAny(), bundle: bundle2, err: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, raw: svid1Raw, wrapped: wrapped, }, { name: "authorizer fails", authorizer: tlsconfig.AuthorizeMemberOf(td2), bundle: bundle1, err: `unexpected trust domain "domain1.test"`, raw: svid1Raw, wrapped: wrapped, }, { name: "wrapped is called", authorizer: tlsconfig.AuthorizeAny(), bundle: bundle1, err: "wrapped called", raw: svid1Raw, wrapped: wrapped, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { verifyPeerCertificate := tlsconfig.WrapVerifyPeerCertificate(testCase.wrapped, testCase.bundle, testCase.authorizer) require.NotNil(t, verifyPeerCertificate) err := verifyPeerCertificate(testCase.raw, [][]*x509.Certificate{}) if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) }) } } func TestTLSHandshake(t *testing.T) { td := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td) bundle1 := ca1.X509Bundle() svid1ID := spiffeid.RequireFromPath(td, "/server") serverSVID := ca1.CreateX509SVID(svid1ID) td2 := spiffeid.RequireTrustDomainFromString("domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() ca3 := test.NewCA(t, td) bundle3 := ca3.Bundle() testCases := []struct { name string serverConfig *tls.Config clientConfig *tls.Config clientErr string serverErr string }{ { name: "success", serverConfig: tlsconfig.TLSServerConfig(serverSVID), clientConfig: tlsconfig.TLSClientConfig(bundle1, tlsconfig.AuthorizeAny()), }, { name: "authentication fails", serverConfig: tlsconfig.TLSServerConfig(serverSVID), clientConfig: tlsconfig.TLSClientConfig(bundle1, tlsconfig.AuthorizeMemberOf(td2)), clientErr: `unexpected trust domain "domain1.test"`, serverErr: "remote error: tls: bad certificate", }, { name: "handshake fails", serverConfig: tlsconfig.TLSServerConfig(serverSVID), clientConfig: tlsconfig.TLSClientConfig(bundle2, tlsconfig.AuthorizeMemberOf(td)), clientErr: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, serverErr: "remote error: tls: bad certificate", }, { name: "unknown authority", serverConfig: tlsconfig.TLSServerConfig(serverSVID), clientConfig: tlsconfig.TLSClientConfig(bundle3, tlsconfig.AuthorizeMemberOf(td)), clientErr: `x509svid: could not verify leaf certificate: x509: certificate signed by unknown authority`, serverErr: "remote error: tls: bad certificate", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { testConnection(t, testCase.serverConfig, testCase.clientConfig, testCase.serverErr, testCase.clientErr) }) } } func TestMTLSHandshake(t *testing.T) { td := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td) bundle1 := ca1.X509Bundle() svid1ID := spiffeid.RequireFromPath(td, "/server") serverSVID := ca1.CreateX509SVID(svid1ID) svid2ID := spiffeid.RequireFromPath(td, "/client") clientSVID := ca1.CreateX509SVID(svid2ID) td2 := spiffeid.RequireTrustDomainFromString("domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() // Create a new bundle with same TD and SVID in order to verify that // presented certificates fails on handshake. ca3 := test.NewCA(t, td) bundle3 := ca3.Bundle() svid3ID := spiffeid.RequireFromPath(td, "/client") client3SVID := ca3.CreateX509SVID(svid3ID) testCases := []struct { name string serverConfig *tls.Config clientConfig *tls.Config serverErr string clientErr string }{ { name: "success", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle1, tlsconfig.AuthorizeAny()), clientConfig: tlsconfig.MTLSClientConfig(clientSVID, bundle1, tlsconfig.AuthorizeAny()), }, { name: "client authentication fails", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle1, tlsconfig.AuthorizeAny()), clientConfig: tlsconfig.MTLSClientConfig(clientSVID, bundle1, tlsconfig.AuthorizeMemberOf(td2)), clientErr: `unexpected trust domain "domain1.test"`, serverErr: "remote error: tls: bad certificate", }, { name: "client handshake fails", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle1, tlsconfig.AuthorizeAny()), clientConfig: tlsconfig.MTLSClientConfig(clientSVID, bundle2, tlsconfig.AuthorizeAny()), clientErr: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, serverErr: "remote error: tls: bad certificate", }, { name: "server authentication", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle1, tlsconfig.AuthorizeMemberOf(td2)), clientConfig: tlsconfig.MTLSClientConfig(clientSVID, bundle1, tlsconfig.AuthorizeAny()), clientErr: "remote error: tls: bad certificate", serverErr: `unexpected trust domain "domain1.test"`, }, { name: "server handshake fails", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle2, tlsconfig.AuthorizeAny()), clientConfig: tlsconfig.MTLSClientConfig(clientSVID, bundle1, tlsconfig.AuthorizeAny()), clientErr: "remote error: tls: bad certificate", serverErr: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, }, { name: "unknown authority", serverConfig: tlsconfig.MTLSServerConfig(serverSVID, bundle1, tlsconfig.AuthorizeAny()), clientConfig: tlsconfig.MTLSClientConfig(client3SVID, bundle3, tlsconfig.AuthorizeAny()), serverErr: "remote error: tls: bad certificate", clientErr: "x509svid: could not verify leaf certificate: x509: certificate signed by unknown authority", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { testConnection(t, testCase.serverConfig, testCase.clientConfig, testCase.serverErr, testCase.clientErr) }) } } func TestMTLSWebHandshake(t *testing.T) { td := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td) bundle1 := ca1.X509Bundle() serverID := spiffeid.RequireFromPath(td, "/server") serverSVID := ca1.CreateX509SVID(serverID) roots, tlsCert := test.CreateWebCredentials(t) roots2 := x509.NewCertPool() roots2.AddCert(serverSVID.Certificates[0]) clientID := spiffeid.RequireFromPath(td, "/client") clientSVID := ca1.CreateX509SVID(clientID) td2 := spiffeid.RequireTrustDomainFromString("domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() // Create a new bundle with same TD and SVID in order to verify that // presented certificates fails on handshake. ca3 := test.NewCA(t, td) svid3ID := spiffeid.RequireFromPath(td, "/client") client3SVID := ca3.CreateX509SVID(svid3ID) testCases := []struct { name string clientConfig *tls.Config clientErr string serverConfig *tls.Config serverErr string }{ { name: "success", clientConfig: tlsconfig.MTLSWebClientConfig(clientSVID, roots), serverConfig: tlsconfig.MTLSWebServerConfig(tlsCert, bundle1, tlsconfig.AuthorizeAny()), }, { name: "server authentication fails", clientConfig: tlsconfig.MTLSWebClientConfig(clientSVID, roots), clientErr: "remote error: tls: bad certificate", serverConfig: tlsconfig.MTLSWebServerConfig(tlsCert, bundle1, tlsconfig.AuthorizeMemberOf(td2)), serverErr: `unexpected trust domain "domain1.test"`, }, { name: "server handshake fails", clientConfig: tlsconfig.MTLSWebClientConfig(clientSVID, roots), clientErr: "remote error: tls: bad certificate", serverConfig: tlsconfig.MTLSWebServerConfig(tlsCert, bundle2, tlsconfig.AuthorizeMemberOf(td2)), serverErr: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, }, { name: "client no valid certificate", clientConfig: tlsconfig.MTLSWebClientConfig(clientSVID, roots2), clientErr: "x509: certificate signed by unknown authority", serverConfig: tlsconfig.MTLSWebServerConfig(tlsCert, bundle1, tlsconfig.AuthorizeAny()), serverErr: "remote error: tls: bad certificate", }, { name: "unknown authority", clientConfig: tlsconfig.MTLSWebClientConfig(client3SVID, roots), serverConfig: tlsconfig.MTLSWebServerConfig(tlsCert, bundle1, tlsconfig.AuthorizeAny()), clientErr: "remote error: tls: bad certificate", serverErr: "x509svid: could not verify leaf certificate: x509: certificate signed by unknown authority", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { testConnection(t, testCase.serverConfig, testCase.clientConfig, testCase.serverErr, testCase.clientErr) }) } } func testConnection(t testing.TB, serverConfig *tls.Config, clientConfig *tls.Config, serverErr string, clientErr string) { ln, err := tls.Listen("tcp", "127.0.0.1:0", serverConfig) require.NoError(t, err) defer ln.Close() errCh := make(chan error, 1) defer func() { err := <-errCh if serverErr != "" { require.EqualError(t, err, serverErr) return } require.NoError(t, err) }() done := make(chan struct{}) defer close(done) go func() { errCh <- func() error { conn, err := ln.Accept() if err != nil { return err } defer func() { <-done conn.Close() }() buf := make([]byte, 1) if _, err = conn.Read(buf); err != nil { return err } _, err = conn.Write([]byte{2}) return err }() }() conn, err := tls.Dial("tcp", ln.Addr().String(), clientConfig) // Only expecting errors on client when a remote error is expected on server side if clientErr != "" && serverErr == "remote error: tls: bad certificate" { if conn != nil { conn.Close() } require.ErrorContains(t, err, clientErr) return } require.NoError(t, err) defer conn.Close() _, err = conn.Write([]byte{1}) require.NoError(t, err) buf := make([]byte, 1) _, err = conn.Read(buf) if clientErr != "" { require.EqualError(t, err, clientErr) return } require.NoError(t, err) require.Equal(t, buf, []byte{2}) } func createTestTLSConfig(base *tls.Config) *tls.Config { tlsCert := tls.Certificate{Certificate: [][]byte{[]byte("body")}} return &tls.Config{ //nolint:gosec // MinVersion it is ok for this test Rand: base.Rand, Time: base.Time, GetConfigForClient: base.GetConfigForClient, NextProtos: base.NextProtos, ServerName: base.ServerName, ClientCAs: base.ClientCAs, CipherSuites: base.CipherSuites, SessionTicketsDisabled: base.SessionTicketsDisabled, SessionTicketKey: base.SessionTicketKey, //nolint:staticcheck // need to ensure this value is copied from the base ClientSessionCache: base.ClientSessionCache, MinVersion: base.MinVersion, MaxVersion: base.MaxVersion, CurvePreferences: base.CurvePreferences, DynamicRecordSizingDisabled: base.DynamicRecordSizingDisabled, Renegotiation: base.Renegotiation, KeyLogWriter: base.KeyLogWriter, // Auth fields Certificates: []tls.Certificate{ tlsCert, }, NameToCertificate: map[string]*tls.Certificate{"cert": &tlsCert}, GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, err error) { return nil, nil }, GetClientCertificate: func(info *tls.CertificateRequestInfo) (certificate *tls.Certificate, err error) { return nil, nil }, VerifyPeerCertificate: nil, RootCAs: x509.NewCertPool(), InsecureSkipVerify: false, ClientAuth: tls.RequestClientCert, } } func createBaseTLSConfig() *tls.Config { return &tls.Config{ Rand: strings.NewReader("my rand"), Time: time.Now().Add(-time.Minute).UTC, GetConfigForClient: func(info *tls.ClientHelloInfo) (config *tls.Config, err error) { return nil, nil }, NextProtos: []string{"nextProtos"}, ServerName: "Server1", ClientCAs: x509.NewCertPool(), CipherSuites: []uint16{12}, SessionTicketsDisabled: true, SessionTicketKey: [32]byte{32}, ClientSessionCache: tls.NewLRUClientSessionCache(32), MinVersion: 999, //nolint:gosec // setting to 999 is OK, for this test MaxVersion: 999, CurvePreferences: []tls.CurveID{tls.CurveP256}, DynamicRecordSizingDisabled: true, Renegotiation: 32, KeyLogWriter: &bytes.Buffer{}, } } func assertUnrelatedFieldsUntouched(t testing.TB, base, wrapped *tls.Config) { assert.Equal(t, base.Rand, wrapped.Rand) assert.NotNil(t, wrapped.Time) assert.NotNil(t, wrapped.GetConfigForClient) assert.Equal(t, base.NextProtos, wrapped.NextProtos) assert.Equal(t, base.ServerName, wrapped.ServerName) assert.Equal(t, base.ClientCAs, wrapped.ClientCAs) assert.Equal(t, base.CipherSuites, wrapped.CipherSuites) assert.Equal(t, base.SessionTicketsDisabled, wrapped.SessionTicketsDisabled) assert.Equal(t, base.SessionTicketKey, wrapped.SessionTicketKey) //nolint:staticcheck // need to assert this field is not inadvertently mutated assert.Equal(t, base.ClientSessionCache, wrapped.ClientSessionCache) assert.Equal(t, base.MinVersion, wrapped.MinVersion) assert.Equal(t, base.MaxVersion, wrapped.MaxVersion) assert.Equal(t, base.CurvePreferences, wrapped.CurvePreferences) assert.Equal(t, base.DynamicRecordSizingDisabled, wrapped.DynamicRecordSizingDisabled) assert.Equal(t, base.Renegotiation, wrapped.Renegotiation) assert.Equal(t, base.KeyLogWriter, wrapped.KeyLogWriter) } type fakeSource struct { err error svid *x509svid.SVID } func (f *fakeSource) GetX509SVID() (*x509svid.SVID, error) { if f.err != nil { return nil, f.err } return f.svid, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/examples_test.go000066400000000000000000000022331474173014300271210ustar00rootroot00000000000000package tlsconfig_test import ( "context" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) func ExampleMTLSServerConfig_fileSource() { td, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: error handling } svid, err := x509svid.Load("svid.pem", "key.pem") if err != nil { // TODO: handle error } bundle, err := x509bundle.Load(td, "cacert.pem") if err != nil { // TODO: handle error } config := tlsconfig.MTLSServerConfig(svid, bundle, tlsconfig.AuthorizeMemberOf(td)) // TODO: use the config config = config } func ExampleMTLSServerConfig_workloadAPISource() { td, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: error handling } source, err := workloadapi.NewX509Source(context.Background()) if err != nil { // TODO: handle error } defer source.Close() config := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeMemberOf(td)) // TODO: use the config config = config } golang-github-spiffe-go-spiffe-2.4.0/v2/spiffetls/tlsconfig/trace.go000066400000000000000000000010141474173014300253360ustar00rootroot00000000000000package tlsconfig import ( "crypto/tls" ) // GetCertificateInfo is an empty placeholder for future expansion type GetCertificateInfo struct { } // GotCertificateInfo provides err and TLS certificate info to Trace type GotCertificateInfo struct { Cert *tls.Certificate Err error } // Trace is the interface to define what functions are triggered when functions // in tlsconfig are called type Trace struct { GetCertificate func(GetCertificateInfo) interface{} GotCertificate func(GotCertificateInfo, interface{}) } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/000077500000000000000000000000001474173014300206735ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/svid/jwtsvid/000077500000000000000000000000001474173014300223655ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/svid/jwtsvid/examples_test.go000066400000000000000000000013101474173014300255640ustar00rootroot00000000000000package jwtsvid_test import ( "context" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) func ExampleParseAndValidate() { td, err := spiffeid.TrustDomainFromString("example.org") if err != nil { // TODO: error handling } token := "TODO" audience := []string{spiffeid.RequireFromPath(td, "/server").String()} jwtSource, err := workloadapi.NewJWTSource(context.TODO()) if err != nil { // TODO: error handling } defer jwtSource.Close() svid, err := jwtsvid.ParseAndValidate(token, jwtSource, audience) if err != nil { // TODO: error handling } // TODO: do something with the JWT-SVID svid = svid } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/jwtsvid/source.go000066400000000000000000000007271474173014300242220ustar00rootroot00000000000000package jwtsvid import ( "context" "github.com/spiffe/go-spiffe/v2/spiffeid" ) // Params are JWT-SVID parameters used when fetching a new JWT-SVID. type Params struct { Audience string ExtraAudiences []string Subject spiffeid.ID } // Source represents a source of JWT-SVIDs. type Source interface { // FetchJWTSVID fetches a JWT-SVID from the source with the given // parameters. FetchJWTSVID(ctx context.Context, params Params) (*SVID, error) } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/jwtsvid/svid.go000066400000000000000000000114351474173014300236650ustar00rootroot00000000000000package jwtsvid import ( "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) var ( allowedSignatureAlgorithms = []jose.SignatureAlgorithm{ jose.RS256, jose.RS384, jose.RS512, jose.ES256, jose.ES384, jose.ES512, jose.PS256, jose.PS384, jose.PS512, } jwtsvidErr = errs.Class("jwtsvid") ) // tokenValidator validates the token and returns the claims type tokenValidator = func(*jwt.JSONWebToken, spiffeid.TrustDomain) (map[string]interface{}, error) // SVID represents a JWT-SVID. type SVID struct { // ID is the SPIFFE ID of the JWT-SVID as present in the 'sub' claim ID spiffeid.ID // Audience is the intended recipients of JWT-SVID as present in the 'aud' claim Audience []string // Expiry is the expiration time of JWT-SVID as present in 'exp' claim Expiry time.Time // Claims is the parsed claims from token Claims map[string]interface{} // Hint is an operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. Hint string // token is the serialized JWT token token string } // ParseAndValidate parses and validates a JWT-SVID token and returns the // JWT-SVID. The JWT-SVID signature is verified using the JWT bundle source. func ParseAndValidate(token string, bundles jwtbundle.Source, audience []string) (*SVID, error) { return parse(token, audience, func(tok *jwt.JSONWebToken, trustDomain spiffeid.TrustDomain) (map[string]interface{}, error) { // Obtain the key ID from the header keyID := tok.Headers[0].KeyID if keyID == "" { return nil, jwtsvidErr.New("token header missing key id") } // Get JWT Bundle bundle, err := bundles.GetJWTBundleForTrustDomain(trustDomain) if err != nil { return nil, jwtsvidErr.New("no bundle found for trust domain %q", trustDomain) } // Find JWT authority using the key ID from the token header authority, ok := bundle.FindJWTAuthority(keyID) if !ok { return nil, jwtsvidErr.New("no JWT authority %q found for trust domain %q", keyID, trustDomain) } // Obtain and verify the token claims using the obtained JWT authority claimsMap := make(map[string]interface{}) if err := tok.Claims(authority, &claimsMap); err != nil { return nil, jwtsvidErr.New("unable to get claims from token: %v", err) } return claimsMap, nil }) } // ParseInsecure parses and validates a JWT-SVID token and returns the // JWT-SVID. The JWT-SVID signature is not verified. func ParseInsecure(token string, audience []string) (*SVID, error) { return parse(token, audience, func(tok *jwt.JSONWebToken, td spiffeid.TrustDomain) (map[string]interface{}, error) { // Obtain the token claims insecurely, i.e. without signature verification claimsMap := make(map[string]interface{}) if err := tok.UnsafeClaimsWithoutVerification(&claimsMap); err != nil { return nil, jwtsvidErr.New("unable to get claims from token: %v", err) } return claimsMap, nil }) } // Marshal returns the JWT-SVID marshaled to a string. The returned value is // the same token value originally passed to ParseAndValidate. func (svid *SVID) Marshal() string { return svid.token } func parse(token string, audience []string, getClaims tokenValidator) (*SVID, error) { // Parse serialized token tok, err := jwt.ParseSigned(token, allowedSignatureAlgorithms) if err != nil { return nil, jwtsvidErr.New("unable to parse JWT token") } // Parse out the unverified claims. We need to look up the key by the trust // domain of the SPIFFE ID. var claims jwt.Claims if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil { return nil, jwtsvidErr.New("unable to get claims from token: %v", err) } switch { case claims.Subject == "": return nil, jwtsvidErr.New("token missing subject claim") case claims.Expiry == nil: return nil, jwtsvidErr.New("token missing exp claim") } spiffeID, err := spiffeid.FromString(claims.Subject) if err != nil { return nil, jwtsvidErr.New("token has an invalid subject claim: %v", err) } // Create generic map of claims claimsMap, err := getClaims(tok, spiffeID.TrustDomain()) if err != nil { return nil, err } // Validate the standard claims. if err := claims.Validate(jwt.Expected{ AnyAudience: audience, Time: time.Now(), }); err != nil { // Convert expected validation errors for pretty errors switch err { case jwt.ErrExpired: err = jwtsvidErr.New("token has expired") case jwt.ErrInvalidAudience: err = jwtsvidErr.New("expected audience in %q (audience=%q)", audience, claims.Audience) } return nil, err } return &SVID{ ID: spiffeID, Audience: claims.Audience, Expiry: claims.Expiry.Time().UTC(), Claims: claimsMap, token: token, }, nil } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/jwtsvid/svid_test.go000066400000000000000000000341421474173014300247240ustar00rootroot00000000000000package jwtsvid_test import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "fmt" "testing" "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/cryptosigner" "github.com/go-jose/go-jose/v4/jwt" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/stretchr/testify/require" ) const hs256Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG" + "4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" var ( key1, _ = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) key2, _ = rsa.GenerateKey(rand.Reader, 2048) testAllowedSignatureAlgorithms = []jose.SignatureAlgorithm{ jose.RS256, jose.RS384, jose.RS512, jose.ES256, jose.ES384, jose.ES512, jose.PS256, jose.PS384, jose.PS512, } ) func TestParseAndValidate(t *testing.T) { // Create numeric dates issuedAt := jwt.NewNumericDate(time.Now()) expiresTime := time.Now().Add(time.Minute) expires := jwt.NewNumericDate(expiresTime) // Create trust domain trustDomain1 := spiffeid.RequireTrustDomainFromString("trustdomain") // Create a bundle and add keys bundle1 := jwtbundle.New(trustDomain1) err := bundle1.AddJWTAuthority("authority1", key1.Public()) require.NoError(t, err) err = bundle1.AddJWTAuthority("authority2", key2.Public()) require.NoError(t, err) testCases := []struct { name string bundle *jwtbundle.Bundle audience []string generateToken func(testing.TB) string err string svid *jwtsvid.SVID }{ { name: "success", bundle: bundle1, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, svid: &jwtsvid.SVID{ ID: spiffeid.RequireFromPath(trustDomain1, "/host"), Audience: []string{"audience"}, Expiry: expiresTime, }, }, { name: "malformed", bundle: bundle1, generateToken: func(tb testing.TB) string { return "invalid token" }, err: "jwtsvid: unable to parse JWT token", }, { name: "unsupported algorithm", bundle: bundle1, generateToken: func(tb testing.TB) string { return hs256Token }, err: "jwtsvid: unable to parse JWT token", }, { name: "missing subject", bundle: bundle1, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, err: "jwtsvid: token missing subject claim", }, { name: "missing expiration claim", bundle: bundle1, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, err: "jwtsvid: token missing exp claim", }, { name: "expired", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)), Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, err: "jwtsvid: token has expired", }, { name: "unexpected audience", bundle: bundle1, audience: []string{"another"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, err: `jwtsvid: expected audience in ["another"] (audience=["audience"])`, }, { name: "invalid subject claim", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: "invalid subject", Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "authority1") }, err: `jwtsvid: token has an invalid subject claim: scheme is missing or invalid`, }, { name: "missing key", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "") }, err: "jwtsvid: token header missing key id", }, { name: "no bundle for trust domain", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: "spiffe://another.domain/host", Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "noAuthority") }, err: `jwtsvid: no bundle found for trust domain "another.domain"`, }, { name: "no bundle for authority", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "noKey") }, err: `jwtsvid: no JWT authority "noKey" found for trust domain "trustdomain"`, }, { name: "mismatched JWT authority", bundle: bundle1, audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key2, "authority1") }, err: "jwtsvid: unable to get claims from token: go-jose/go-jose: error in cryptographic primitive", }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { // Generate token token := testCase.generateToken(t) // Parse and validate svid, err := jwtsvid.ParseAndValidate(token, testCase.bundle, testCase.audience) // Verify returned error, in case it is expected if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) // Verify returned svid require.Equal(t, testCase.svid.ID, svid.ID) require.Equal(t, testCase.svid.Expiry.Unix(), svid.Expiry.Unix()) require.Equal(t, testCase.svid.Audience, svid.Audience) claims := parseToken(t, token) require.Equal(t, claims, svid.Claims) }) } } func TestParseInsecure(t *testing.T) { // Create numeric dates issuedAt := jwt.NewNumericDate(time.Now()) expiresTime := time.Now().Add(time.Minute) expires := jwt.NewNumericDate(expiresTime) // Create trust domain trustDomain1 := spiffeid.RequireTrustDomainFromString("trustdomain") testCases := []struct { name string audience []string generateToken func(testing.TB) string err string svid *jwtsvid.SVID }{ { name: "success", generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, svid: &jwtsvid.SVID{ ID: spiffeid.RequireFromPath(trustDomain1, "/host"), Audience: []string{"audience"}, Expiry: expiresTime, }, }, { name: "malformed", generateToken: func(tb testing.TB) string { return "invalid token" }, err: "jwtsvid: unable to parse JWT token", }, { name: "invalid algorithm", generateToken: func(tb testing.TB) string { return hs256Token }, err: "jwtsvid: unable to parse JWT token", }, { name: "missing subject claim", generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, err: "jwtsvid: token missing subject claim", }, { name: "missing expiration claim", generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, err: "jwtsvid: token missing exp claim", }, { name: "expired", audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)), Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, err: "jwtsvid: token has expired", }, { name: "unexpected audience", audience: []string{"another"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, err: `jwtsvid: expected audience in ["another"] (audience=["audience"])`, }, { name: "invalid subject claim", audience: []string{"audience"}, generateToken: func(tb testing.TB) string { claims := jwt.Claims{ Subject: "invalid subject", Issuer: "issuer", Expiry: expires, Audience: []string{"audience"}, IssuedAt: issuedAt, } return generateToken(tb, claims, key1, "key1") }, err: `jwtsvid: token has an invalid subject claim: scheme is missing or invalid`, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { // Create token token := testCase.generateToken(t) // Call ParseInsecure svid, err := jwtsvid.ParseInsecure(token, testCase.audience) // Verify returned error, in case it is expected if testCase.err != "" { require.EqualError(t, err, testCase.err) return } require.NoError(t, err) // Verify SVID require.Equal(t, testCase.svid.ID, svid.ID) require.Equal(t, testCase.svid.Expiry.Unix(), svid.Expiry.Unix()) require.Equal(t, testCase.svid.Audience, svid.Audience) claims := parseToken(t, token) require.Equal(t, claims, svid.Claims) }) } } func TestMarshal(t *testing.T) { // Generate trust domain trustDomain1 := spiffeid.RequireTrustDomainFromString("trustdomain") // Generate Token claims := jwt.Claims{ Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), Issuer: "issuer", Expiry: jwt.NewNumericDate(time.Now()), Audience: []string{"audience"}, IssuedAt: jwt.NewNumericDate(time.Now().Add(time.Minute)), } token := generateToken(t, claims, key1, "key1") // Create SVID svid, err := jwtsvid.ParseInsecure(token, []string{"audience"}) require.NoError(t, err) // Validate token is returned require.Equal(t, token, svid.Marshal()) // Update SVID does not affect token svid.ID = spiffeid.RequireFromPath(trustDomain1, "/host2") require.Equal(t, token, svid.Marshal()) // Empty Marshall when no token svid = &jwtsvid.SVID{} require.Empty(t, svid.Marshal()) } func parseToken(t testing.TB, token string) map[string]interface{} { tok, err := jwt.ParseSigned(token, testAllowedSignatureAlgorithms) require.NoError(t, err) claimsMap := make(map[string]interface{}) err = tok.UnsafeClaimsWithoutVerification(&claimsMap) require.NoError(t, err) return claimsMap } // Generate generates a signed string token func generateToken(tb testing.TB, claims jwt.Claims, signer crypto.Signer, keyID string) string { // Get signer algorithm alg, err := getSignerAlgorithm(signer) require.NoError(tb, err) // Create signer using crypto.Signer and its algorithm along with provided key ID jwtSigner, err := jose.NewSigner( jose.SigningKey{ Algorithm: alg, Key: jose.JSONWebKey{ Key: cryptosigner.Opaque(signer), KeyID: keyID, }, }, new(jose.SignerOptions).WithType("JWT"), ) require.NoError(tb, err) // Sign and serialize token token, err := jwt.Signed(jwtSigner).Claims(claims).Serialize() require.NoError(tb, err) return token } // getSignerAlgorithm deduces signer algorithm and return it func getSignerAlgorithm(signer crypto.Signer) (jose.SignatureAlgorithm, error) { switch publicKey := signer.Public().(type) { case *rsa.PublicKey: // Prevent the use of keys smaller than 2048 bits if publicKey.Size() < 256 { return "", fmt.Errorf("unsupported RSA key size: %d", publicKey.Size()) } return jose.RS256, nil case *ecdsa.PublicKey: params := publicKey.Params() switch params.BitSize { case 256: return jose.ES256, nil case 384: return jose.ES384, nil default: return "", fmt.Errorf("unable to determine signature algorithm for EC public key size %d", params.BitSize) } case ed25519.PublicKey: return jose.EdDSA, nil default: return "", fmt.Errorf("unable to determine signature algorithm for public key type %T", publicKey) } } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/000077500000000000000000000000001474173014300222665ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/source.go000066400000000000000000000002551474173014300241170ustar00rootroot00000000000000package x509svid // Source represents a source of X509-SVIDs. type Source interface { // GetX509SVID returns an X509-SVID from the source. GetX509SVID() (*SVID, error) } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/svid.go000066400000000000000000000165371474173014300235760ustar00rootroot00000000000000package x509svid import ( "bytes" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/x509" "os" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) // SVID represents a SPIFFE X509-SVID. type SVID struct { // ID is the SPIFFE ID of the X509-SVID. ID spiffeid.ID // Certificates are the X.509 certificates of the X509-SVID. The leaf // certificate is the X509-SVID certificate. Any remaining certificates ( // if any) chain the X509-SVID certificate back to a X.509 root for the // trust domain. Certificates []*x509.Certificate // PrivateKey is the private key for the X509-SVID. PrivateKey crypto.Signer // Hint is an operator-specified string used to provide guidance on how this // identity should be used by a workload when more than one SVID is returned. Hint string } // Load loads the X509-SVID from PEM encoded files on disk. certFile and // keyFile may be the same file. func Load(certFile, keyFile string) (*SVID, error) { certBytes, err := os.ReadFile(certFile) if err != nil { return nil, x509svidErr.New("cannot read certificate file: %w", err) } keyBytes, err := os.ReadFile(keyFile) if err != nil { return nil, x509svidErr.New("cannot read key file: %w", err) } return Parse(certBytes, keyBytes) } // Parse parses the X509-SVID from PEM blocks containing certificate and key // bytes. The certificate must be one or more PEM blocks with ASN.1 DER. The // key must be a PEM block with PKCS#8 ASN.1 DER. func Parse(certBytes, keyBytes []byte) (*SVID, error) { certs, err := pemutil.ParseCertificates(certBytes) if err != nil { return nil, x509svidErr.New("cannot parse PEM encoded certificate: %v", err) } privateKey, err := pemutil.ParsePrivateKey(keyBytes) if err != nil { return nil, x509svidErr.New("cannot parse PEM encoded private key: %v", err) } return newSVID(certs, privateKey) } // ParseRaw parses the X509-SVID from certificate and key bytes. The // certificate must be ASN.1 DER (concatenated with no intermediate // padding if there are more than one certificate). The key must be a PKCS#8 // ASN.1 DER. func ParseRaw(certBytes, keyBytes []byte) (*SVID, error) { certificates, err := x509.ParseCertificates(certBytes) if err != nil { return nil, x509svidErr.New("cannot parse DER encoded certificate: %v", err) } privateKey, err := x509.ParsePKCS8PrivateKey(keyBytes) if err != nil { return nil, x509svidErr.New("cannot parse DER encoded private key: %v", err) } return newSVID(certificates, privateKey) } // Marshal marshals the X509-SVID and returns PEM encoded blocks for the SVID // and private key. func (s *SVID) Marshal() ([]byte, []byte, error) { if len(s.Certificates) == 0 { return nil, nil, x509svidErr.New("no certificates to marshal") } certBytes := pemutil.EncodeCertificates(s.Certificates) keyBytes, err := pemutil.EncodePKCS8PrivateKey(s.PrivateKey) if err != nil { return nil, nil, x509svidErr.New("cannot encode private key: %v", err) } return certBytes, keyBytes, nil } // MarshalRaw marshals the X509-SVID and returns ASN.1 DER for the certificates // (concatenated with no intermediate padding) and PKCS8 ASN1.DER for the // private key. func (s *SVID) MarshalRaw() ([]byte, []byte, error) { key, err := x509.MarshalPKCS8PrivateKey(s.PrivateKey) if err != nil { return nil, nil, x509svidErr.New("cannot marshal private key: %v", err) } if len(s.Certificates) == 0 { return nil, nil, x509svidErr.New("no certificates to marshal") } certs := x509util.ConcatRawCertsFromCerts(s.Certificates) return certs, key, nil } // GetX509SVID returns the X509-SVID. It implements the Source interface. func (s *SVID) GetX509SVID() (*SVID, error) { return s, nil } func newSVID(certificates []*x509.Certificate, privateKey crypto.PrivateKey) (*SVID, error) { spiffeID, err := validateCertificates(certificates) if err != nil { return nil, x509svidErr.New("certificate validation failed: %v", err) } signer, err := validatePrivateKey(privateKey, certificates[0]) if err != nil { return nil, x509svidErr.New("private key validation failed: %v", err) } return &SVID{ Certificates: certificates, PrivateKey: signer, ID: *spiffeID, }, nil } // validate the slice of certificates constitutes a valid SVID chain according // to the spiffe standard and returns the spiffe id of the leaf certificate func validateCertificates(certificates []*x509.Certificate) (*spiffeid.ID, error) { if len(certificates) == 0 { return nil, errs.New("no certificates found") } leafID, err := validateLeafCertificate(certificates[0]) if err != nil { return nil, err } err = validateSigningCertificates(certificates[1:]) if err != nil { return nil, err } return leafID, nil } func validateLeafCertificate(leaf *x509.Certificate) (*spiffeid.ID, error) { leafID, err := IDFromCert(leaf) if err != nil { return nil, errs.New("cannot get leaf certificate SPIFFE ID: %v", err) } if leaf.IsCA { return nil, errs.New("leaf certificate must not have CA flag set to true") } err = validateKeyUsage(leaf) if err != nil { return nil, err } return &leafID, err } func validateSigningCertificates(signingCerts []*x509.Certificate) error { for _, cert := range signingCerts { if !cert.IsCA { return errs.New("signing certificate must have CA flag set to true") } if cert.KeyUsage&x509.KeyUsageCertSign == 0 { return errs.New("signing certificate must have 'keyCertSign' set as key usage") } } return nil } func validateKeyUsage(leaf *x509.Certificate) error { switch { case leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0: return errs.New("leaf certificate must have 'digitalSignature' set as key usage") case leaf.KeyUsage&x509.KeyUsageCertSign > 0: return errs.New("leaf certificate must not have 'keyCertSign' set as key usage") case leaf.KeyUsage&x509.KeyUsageCRLSign > 0: return errs.New("leaf certificate must not have 'cRLSign' set as key usage") } return nil } func validatePrivateKey(privateKey crypto.PrivateKey, leaf *x509.Certificate) (crypto.Signer, error) { if privateKey == nil { return nil, errs.New("no private key found") } matched, err := keyMatches(privateKey, leaf.PublicKey) if err != nil { return nil, err } if !matched { return nil, errs.New("leaf certificate does not match private key") } signer, ok := privateKey.(crypto.Signer) if !ok { return nil, errs.New("expected crypto.Signer; got %T", privateKey) } return signer, nil } func keyMatches(privateKey crypto.PrivateKey, publicKey crypto.PublicKey) (bool, error) { switch privateKey := privateKey.(type) { case *rsa.PrivateKey: rsaPublicKey, ok := publicKey.(*rsa.PublicKey) return ok && rsaPublicKeyEqual(&privateKey.PublicKey, rsaPublicKey), nil case *ecdsa.PrivateKey: ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey) return ok && ecdsaPublicKeyEqual(&privateKey.PublicKey, ecdsaPublicKey), nil case ed25519.PrivateKey: ed25519PublicKey, ok := publicKey.(ed25519.PublicKey) return ok && bytes.Equal(privateKey.Public().(ed25519.PublicKey), ed25519PublicKey), nil default: return false, errs.New("unsupported private key type %T", privateKey) } } func rsaPublicKeyEqual(a, b *rsa.PublicKey) bool { return a.E == b.E && a.N.Cmp(b.N) == 0 } func ecdsaPublicKeyEqual(a, b *ecdsa.PublicKey) bool { return a.Curve == b.Curve && a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/svid_test.go000066400000000000000000000306521474173014300246270ustar00rootroot00000000000000package x509svid_test import ( "crypto/x509" "errors" "os" "testing" "github.com/spiffe/go-spiffe/v2/internal/pemutil" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( keyRSA = "testdata/key-pkcs8-rsa.pem" certSingle = "testdata/good-leaf-only.pem" leafNoDigitalSignature = "testdata/wrong-leaf-no-digital-signature.pem" leafCRLSign = "testdata/wrong-leaf-crl-sign.pem" leafCertSign = "testdata/wrong-leaf-cert-sign.pem" leafCAtrue = "testdata/wrong-leaf-ca-true.pem" leafEmptyID = "testdata/wrong-leaf-empty-id.pem" signNoCA = "testdata/wrong-intermediate-no-ca.pem" signNoKeyCertSign = "testdata/wrong-intermediate-no-key-cert-sign.pem" keyECDSA = "testdata/key-pkcs8-ecdsa.pem" certMultiple = "testdata/good-leaf-and-intermediate.pem" certAndKey = "testdata/good-cert-and-key.pem" keyAndCert = "testdata/good-key-and-cert.pem" notPEM = "testdata/not-pem" corruptCert = "testdata/corrupt-cert.pem" corruptKey = "testdata/corrupt-key.pem" ) func TestLoad_Succeeds(t *testing.T) { svid, err := x509svid.Load(certSingle, keyRSA) require.NoError(t, err) require.NotNil(t, svid) require.Equal(t, svid.ID.String(), "spiffe://example.org/workload-1") } func TestLoad_FailsCannotReadCertFile(t *testing.T) { svid, err := x509svid.Load("testdata/non-existent.pem", keyRSA) require.Error(t, err) require.Nil(t, svid) require.Contains(t, err.Error(), "cannot read certificate file:") require.True(t, errors.Is(err, os.ErrNotExist)) } func TestLoad_FailsCannotReadKeyFile(t *testing.T) { svid, err := x509svid.Load(certSingle, "testdata/non-existent.pem") require.Error(t, err) require.Nil(t, svid) require.Contains(t, err.Error(), "cannot read key file:") require.True(t, errors.Is(err, os.ErrNotExist)) } func TestParse(t *testing.T) { tests := []struct { name string keyPath string certsPath string expID spiffeid.ID expNumCerts int expErrContains string }{ { name: "Single certificate and key", keyPath: keyRSA, certsPath: certSingle, expID: spiffeid.RequireFromString("spiffe://example.org/workload-1"), expNumCerts: 1, }, { name: "Certificate with intermediate and key", keyPath: keyECDSA, certsPath: certMultiple, expID: spiffeid.RequireFromString("spiffe://example.org/workload-1"), expNumCerts: 2, }, { name: "Key and certificate in the same byte array", keyPath: keyAndCert, certsPath: keyAndCert, expID: spiffeid.RequireFromString("spiffe://example.org/workload-1"), expNumCerts: 1, }, { name: "Certificate and Key in the same byte array", keyPath: certAndKey, certsPath: certAndKey, expID: spiffeid.RequireFromString("spiffe://example.org/workload-1"), expNumCerts: 1, }, { name: "Missing certificate", keyPath: keyRSA, certsPath: keyRSA, expErrContains: "x509svid: certificate validation failed: no certificates found", }, { name: "Missing private key", keyPath: certSingle, certsPath: certSingle, expErrContains: "x509svid: private key validation failed: no private key found", }, { name: "Private key not PEM", keyPath: notPEM, certsPath: certSingle, expErrContains: "x509svid: cannot parse PEM encoded private key: no PEM blocks found", }, { name: "Certificate not PEM", keyPath: keyRSA, certsPath: notPEM, expErrContains: "x509svid: cannot parse PEM encoded certificate: no PEM blocks found", }, { name: "Corrupt private key", keyPath: corruptKey, certsPath: certSingle, expErrContains: "x509svid: cannot parse PEM encoded private key: asn1: structure error:", }, { name: "Corrupt certificate", keyPath: keyRSA, certsPath: corruptCert, expErrContains: "x509svid: cannot parse PEM encoded certificate: x509: malformed certificate", }, { name: "Certificate does not match private key", keyPath: keyRSA, certsPath: certMultiple, expErrContains: "x509svid: private key validation failed: leaf certificate does not match private key", }, { name: "Certificate without SPIFFE ID", keyPath: keyRSA, certsPath: leafEmptyID, expErrContains: "x509svid: certificate validation failed: cannot get leaf certificate SPIFFE ID: certificate contains no URI SAN", }, { name: "Leaf certificate with CA flag set to true", certsPath: leafCAtrue, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: leaf certificate must not have CA flag set to true", }, { name: "Leaf certificate without digitalSignature as key usage", certsPath: leafNoDigitalSignature, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: leaf certificate must have 'digitalSignature' set as key usage", }, { name: "Leaf certificate with certSign as key usage", certsPath: leafCertSign, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: leaf certificate must not have 'keyCertSign' set as key usage", }, { name: "Leaf certificate with cRLSign as key usage", certsPath: leafCRLSign, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: leaf certificate must not have 'cRLSign' set as key usage", }, { name: "Signing certificate without CA flag", certsPath: signNoCA, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: signing certificate must have CA flag set to true", }, { name: "Signing certificate without 'keyCertSign' usage", certsPath: signNoKeyCertSign, keyPath: keyRSA, expErrContains: "x509svid: certificate validation failed: signing certificate must have 'keyCertSign' set as key usage", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { certBytes, err := os.ReadFile(test.certsPath) require.NoError(t, err) keyBytes, err := os.ReadFile(test.keyPath) require.NoError(t, err) svid, err := x509svid.Parse(certBytes, keyBytes) if test.expErrContains != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) assert.NotNil(t, svid) assert.Equal(t, test.expID, svid.ID) assert.Len(t, svid.Certificates, test.expNumCerts) assert.Equal(t, svid.PrivateKey.Public(), svid.Certificates[0].PublicKey) }) } } func TestGetX509SVID(t *testing.T) { s, err := x509svid.Load(certSingle, keyRSA) require.NoError(t, err) svid, err := s.GetX509SVID() require.NoError(t, err) assert.Equal(t, s, svid) } func TestMarshal(t *testing.T) { tests := []struct { name string keyPath string certsPath string modifySVID func(*x509svid.SVID) expErrContains string }{ { name: "Single certificate and key", keyPath: keyRSA, certsPath: certSingle, }, { name: "Multiple certificates and key", keyPath: keyECDSA, certsPath: certMultiple, }, { name: "Fails to encode private key", keyPath: keyRSA, certsPath: certSingle, expErrContains: "cannot encode private key", modifySVID: func(s *x509svid.SVID) { s.PrivateKey = nil // Set private key to nil to force an error }, }, { name: "Fails to marshal certificates", keyPath: keyRSA, certsPath: certSingle, expErrContains: "no certificates to marshal", modifySVID: func(s *x509svid.SVID) { s.Certificates = nil // Set certificates to nil to force an error }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { s, err := x509svid.Load(test.certsPath, test.keyPath) require.NoError(t, err) if test.modifySVID != nil { test.modifySVID(s) } certs, key, err := s.Marshal() if test.expErrContains != "" { require.Error(t, err) require.Nil(t, certs) require.Nil(t, key) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) require.NotNil(t, certs) require.NotNil(t, key) expCerts, err := os.ReadFile(test.certsPath) require.NoError(t, err) assert.Equal(t, expCerts, certs) expKey, err := os.ReadFile(test.keyPath) require.NoError(t, err) assert.Equal(t, expKey, key) }) } } func TestMarshalRaw(t *testing.T) { tests := []struct { name string keyPath string certsPath string modifySVID func(*x509svid.SVID) expErrContains string }{ { name: "Single certificate and key", keyPath: keyRSA, certsPath: certSingle, }, { name: "Multiple certificates and key", keyPath: keyECDSA, certsPath: certMultiple, }, { name: "Fails to marshal private key", keyPath: keyRSA, certsPath: certSingle, expErrContains: "cannot marshal private key", modifySVID: func(s *x509svid.SVID) { s.PrivateKey = nil // Set private key to nil to force an error }, }, { name: "Fails to marshal certificates", keyPath: keyRSA, certsPath: certSingle, expErrContains: "no certificates to marshal", modifySVID: func(s *x509svid.SVID) { s.Certificates = nil // Set private key to nil to force an error }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { s, err := x509svid.Load(test.certsPath, test.keyPath) require.NoError(t, err) if test.modifySVID != nil { test.modifySVID(s) } rawCert, rawKey, err := s.MarshalRaw() if test.expErrContains != "" { require.Error(t, err) require.Nil(t, rawCert) require.Nil(t, rawKey) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) require.NotNil(t, rawCert) require.NotNil(t, rawKey) expRawCert := loadRawCertificates(t, test.certsPath) assert.Equal(t, expRawCert, rawCert) expRawKey := loadRawKey(t, test.keyPath) assert.Equal(t, expRawKey, rawKey) }) } } func TestParseRaw(t *testing.T) { tests := []struct { name string keyPath string certsPath string rawCerts []byte rawKey []byte expErrContains string }{ { name: "Single certificate and key", keyPath: keyRSA, certsPath: certSingle, rawCerts: loadRawCertificates(t, certSingle), rawKey: loadRawKey(t, keyRSA), }, { name: "Multiple certificates and key", keyPath: keyECDSA, certsPath: certMultiple, rawCerts: loadRawCertificates(t, certMultiple), rawKey: loadRawKey(t, keyECDSA), }, { name: "Certificate bytes are not DER encoded", rawCerts: []byte("not-DER-encoded"), rawKey: loadRawKey(t, keyRSA), expErrContains: "x509svid: cannot parse DER encoded certificate", }, { name: "Key bytes are not DER encoded", rawCerts: loadRawCertificates(t, certSingle), rawKey: []byte("not-DER-encoded"), expErrContains: "x509svid: cannot parse DER encoded private key", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { svid, err := x509svid.ParseRaw(test.rawCerts, test.rawKey) if test.expErrContains != "" { require.Error(t, err) require.Nil(t, svid) assert.Contains(t, err.Error(), test.expErrContains) return } require.NoError(t, err) require.NotNil(t, svid) expectedSVID, err := x509svid.Load(test.certsPath, test.keyPath) require.NoError(t, err) assert.Equal(t, expectedSVID, svid) }) } } func loadRawCertificates(t *testing.T, path string) []byte { certsBytes, err := os.ReadFile(path) require.NoError(t, err) certs, err := pemutil.ParseCertificates(certsBytes) require.NoError(t, err) var rawBytes []byte for _, cert := range certs { rawBytes = append(rawBytes, cert.Raw...) } return rawBytes } func loadRawKey(t *testing.T, path string) []byte { keyBytes, err := os.ReadFile(path) require.NoError(t, err) key, err := pemutil.ParsePrivateKey(keyBytes) require.NoError(t, err) rawKey, err := x509.MarshalPKCS8PrivateKey(key) require.NoError(t, err) return rawKey } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/000077500000000000000000000000001474173014300240775ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/corrupt-cert.pem000066400000000000000000000001031474173014300272250ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- Q09SUlVQVA== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/corrupt-key.pem000066400000000000000000000001031474173014300270600ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- Q09SUlVQVA== -----END PRIVATE KEY----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/good-cert-and-key.pem000066400000000000000000000017521474173014300300200ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICBjCCAYygAwIBAgIQNj0chc2GkwvkNG0vVbWuADAKBggqhkjOPQQDAzAeMQsw CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIwMDMyNDE0MTM0MVoXDTIw MDMyNDE1MTM1MVowHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAE8ST0O2obQ9VYEyFEbiyIML7naZtAtA9DU9df zYCeA4fHplrgk0ZL+MBXOMjCEo0fLX+jxqMpLuPy7wGfwlqRaKOBrDCBqTAOBgNV HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud EwEB/wQCMAAwHQYDVR0OBBYEFB3UYIIkxjMf9rh9tvwj86Y24+6gMB8GA1UdIwQY MBaAFNM1QzCBy3PuB2d3zJi6GSEqqVF5MCoGA1UdEQQjMCGGH3NwaWZmZTovL2V4 YW1wbGUub3JnL3dvcmtsb2FkLTEwCgYIKoZIzj0EAwMDaAAwZQIwKhlIltKg+K/3 W05Snv56s7X9NuUDKHjaCQsutyIiYxbxQz5jZgjafMusAwr+lMQkAjEAsY4Omqtj MT7lix7GtnRkvgmaWRTyooxyR1C2w8PYS6lSo6FJCIV6e1EBvryj6Vm1 -----END CERTIFICATE----- -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdCYVeDmdr1H8Fzpn MSfBa0zAAy+fnp2z84R4v9Ff7nGhRANCAATxJPQ7ahtD1VgTIURuLIgwvudpm0C0 D0NT11/NgJ4Dh8emWuCTRkv4wFc4yMISjR8tf6PGoyku4/LvAZ/CWpFo -----END PRIVATE KEY----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/good-key-and-cert.pem000066400000000000000000000017521474173014300300200ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdCYVeDmdr1H8Fzpn MSfBa0zAAy+fnp2z84R4v9Ff7nGhRANCAATxJPQ7ahtD1VgTIURuLIgwvudpm0C0 D0NT11/NgJ4Dh8emWuCTRkv4wFc4yMISjR8tf6PGoyku4/LvAZ/CWpFo -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIICBjCCAYygAwIBAgIQNj0chc2GkwvkNG0vVbWuADAKBggqhkjOPQQDAzAeMQsw CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIwMDMyNDE0MTM0MVoXDTIw MDMyNDE1MTM1MVowHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAE8ST0O2obQ9VYEyFEbiyIML7naZtAtA9DU9df zYCeA4fHplrgk0ZL+MBXOMjCEo0fLX+jxqMpLuPy7wGfwlqRaKOBrDCBqTAOBgNV HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud EwEB/wQCMAAwHQYDVR0OBBYEFB3UYIIkxjMf9rh9tvwj86Y24+6gMB8GA1UdIwQY MBaAFNM1QzCBy3PuB2d3zJi6GSEqqVF5MCoGA1UdEQQjMCGGH3NwaWZmZTovL2V4 YW1wbGUub3JnL3dvcmtsb2FkLTEwCgYIKoZIzj0EAwMDaAAwZQIwKhlIltKg+K/3 W05Snv56s7X9NuUDKHjaCQsutyIiYxbxQz5jZgjafMusAwr+lMQkAjEAsY4Omqtj MT7lix7GtnRkvgmaWRTyooxyR1C2w8PYS6lSo6FJCIV6e1EBvryj6Vm1 -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/good-leaf-and-intermediate.pem000066400000000000000000000027521474173014300316550ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICBzCCAY2gAwIBAgIRAJ4TY883AKQzW4gEzxTP5ekwCgYIKoZIzj0EAwMwHjEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMDAzMjQxNDA3MzBaFw0y MDAzMjQxNTA3NDBaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG ByqGSM49AgEGCCqGSM49AwEHA0IABI6NiQ4HU4ZS8koPLevFZOzNPJRBGmsr6CMj qww2LVQDxF2/QiJUtVf6yPhtXYI/uWh8yBvRNxLfMmscAYf1gBOjgawwgakwDgYD VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV HRMBAf8EAjAAMB0GA1UdDgQWBBSyUx1gdahNKPD13hnr5SJq7QdHqzAfBgNVHSME GDAWgBR1/vyENH1J5W0G0zexqR4Q2UVIGjAqBgNVHREEIzAhhh9zcGlmZmU6Ly9l eGFtcGxlLm9yZy93b3JrbG9hZC0xMAoGCCqGSM49BAMDA2gAMGUCMQDsFcGtMDZi w2aypdxvr1tvN/Opahi4zJ3DIlfAIhSNQ8gDp7LS7u06Ob/6ouh/1c4CMEpVoyS4 ZTnENACY3TXXmRt/mZsXyyHSgQGyEFqmvehpsDLIAL2+nKLfcyzENZn4Rg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIB/DCCAYOgAwIBAgIQBRQ/CSzrgkGFpQ7mVSr7yjAKBggqhkjOPQQDAzAeMQsw CQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMB4XDTIwMDMyNDE0MDUwM1oXDTIw MDMyNTE0MDUxM1owHjELMAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTB2MBAG ByqGSM49AgEGBSuBBAAiA2IABExZXvtfcorJWkVs8pdriln6Y5aewz+r0ibFXdKt lHOg31MQsnZkh3wlOxuVwwyuuTlpb8LwIyOhuYbb6lbWHDDhSHXh3ye021PifZGc X8pHXRQk6D8SP1+260sOmCTI1qOBhTCBgjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T AQH/BAUwAwEB/zAdBgNVHQ4EFgQUdf78hDR9SeVtBtM3sakeENlFSBowHwYDVR0j BBgwFoAUh6XzV6LwNazA+GTEVOdu07o5yOgwHwYDVR0RBBgwFoYUc3BpZmZlOi8v ZXhhbXBsZS5vcmcwCgYIKoZIzj0EAwMDZwAwZAIwApQtPxHc1mx+aOES1D0RFttH IyVP9szoPz1wjSFXnxA8zY8ikVGx6FdviaHe4RXaAjBA3tN4AJf/yBzJU7cStXR8 Y8l67Q87PUKX5SyAdGgoqVX2V+i/kH1ZFt0BikmRWtA= -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/good-leaf-only.pem000066400000000000000000000022071474173014300274170ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAPKy5MTuxkq7MA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTkwMDM3WhcNMjUwMzI5 MTkwMDM3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCB4AwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAsgNGb0lDyvPYsOMtNCZxpEQh WDM4nPc/fWmRQv2GE24VPDxTa+KF8QASuK4SK3aHgixIAUiugOpFr5ZuyX7nWhcb 5lPXOfuAB7SL5rq9E4kaikhNiJOCDxMORNZmpe9fii+l4clrmqIaryDHOhCGZFvr ppPOxLFPY1pnFBUtEjrM8TnpVMdV6jB4im7lKfOse/1BhzRQQ/dJg4EFk1S9pHse ryc0wJMM685dgAmELRigWx2IpU7Ma/09WlcCG7AmToeXPcEOa0xFS3CNEat8+bv+ BaWSJ31+se8tDTjjEfV3M23OdUmyv29aTb74wd33u6+jsliFK5psZKbcg3hGCA== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/key-pkcs8-ecdsa.pem000066400000000000000000000003611474173014300274750ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmgpKax+BCcPM3gcZ 1zvl2zquf76KwRhQWfQJQTKhGfKhRANCAASOjYkOB1OGUvJKDy3rxWTszTyUQRpr K+gjI6sMNi1UA8Rdv0IiVLVX+sj4bV2CP7lofMgb0TcS3zJrHAGH9YAT -----END PRIVATE KEY----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/key-pkcs8-rsa.pem000066400000000000000000000032501474173014300272030ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+52SxQMLCsBvk CDnNtFoy/Q5Zx6F83/V6OsMlS7SCadgY6uzTF0w8IhpweQ4SbSdInZvflEQFvDXc VMIcdoUf20LQRmvP4WSaGg/YdNwZGyKJt8G9GnJ3fjqw/omqhylM2WOFDFuEoLLh aQBL8oqOsPw12RDzN0yHy8HMSTMPCNuNufetaC8dwE5Tdm7aafn8s/gHyheUYzif tkxYrfmz7JcrObCSW2lC1VVOa2QKZMt9yQGxKAERKdGAiNj+N/Amqu6C892bHROg g2WOJqHPRRyLeFEuFhzfMCxAmgVdo2ubqoUK05H/Ecr9EIpj1scoS9q9XH4XZa/H cymF13O1AgMBAAECggEAKb363G8mpt9PGetaiEoZNvDyRFtMSjvGNzXGGc+V5rYy FDC6G+YKO1PRqCowE4NehZhAzwBiZ0aiGE1ILGUV8sNEtrnPNSM5liCAunnC7pJq WOafLDQuN0aYmr7tZyYqt60I/7yV/kgNFRCaigC8nzq/yx5tgGDlEQRSmdKfoXeS 2mmHtpB06bMDU+cptfsG1S/eChVAngJcKAYdIukud5GGgsYp3TiXxM/IZ2wapclT T4ATAERvysT58sYR4sCGpphFzsAkjd6oFZoADSw58bKrq8Afb/txjHdXwhV+crPu BDbozuk5F2r9U/cGtpTI3Ay1T+VHOHkKuUJCYz97gQKBgQD72Hr+DxpBkn4cgPGx jBe7/Uixs5+cQL44lqIEOd6uLLE+xrPOZgpuOqz7FPVcAnBgh+knf1Kz5SgQwrTU 22SlYPsS2+D3Rcnp5UMfqfwFq1/X9UGF62l3VHwgs40iP4k28H9Y4GYeXRNIigyu CDImn1FU6cJx1w1aUnYrIoEDMQKBgQDCDY/T/mm8DmLeV6wxAzSJYfKhgrvZ/Jc0 qGQdp9yGpZDqV1zRtoJE7jno1YRt01ye0xXLZYsgYbG04kWNSTDy9yfLPBDN28m/ yiYNz6ckYI2HNts8BSIGjF3cuF4XGaEkaMRlnx5iAb9BZ6692itWGJ4skGuUdKXh cFNt1hovxQKBgQDz6QW1aQsU1rws/vUV74hNGVF+SWkb+9g/FiRz01hHeCFxmyAp lcokV8+QnQvEwNf1pau6BSYj9JyJHwnj3Vfsk7CW4z1OPtj/HnuT/x2GoCODFJUR Dp5mY/yT71GRCdfqzaGIgkxku+AhMRJ2uplXrpUml/8qIg1cnOC1hgVNAQKBgDQs 21AfpqRGpfSsVAL1nqmVqVwdv45z6N/iqtCCcrvNRnKLvMwyK0KHXxCoYCv7WXrm vIRssASr39EHybWcSUn6hDuT0dzXzJ4Bp0utWn5ga41AhZ/UrXpfQVl4ROwnGvmk JbJBHzUwzRCz5Prs7xv+EIFg71wCJRvBTN1KZM4VAoGBAK7ud6KNqLtesyFQxmlA ccHLqIFDVZy30nghXEHGIhUwEXstA6wb0R563svaBVFd+q5SAtfRKPoWNz+yxnUn r9DvXKB/A2jFzmZ5HKQoc6YD5U+jIwWX9HBw/UK1pNIemIbnhkJCbqaC09nZ03nM 7OCe8CKM5vdVTa2hHowYf3ci -----END PRIVATE KEY----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/not-pem000066400000000000000000000000101474173014300253700ustar00rootroot00000000000000not-pem golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-intermediate-no-ca.pem000066400000000000000000000043761474173014300314130ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAPKy5MTuxkq7MA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTkwMDM3WhcNMjUwMzI5 MTkwMDM3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCB4AwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAsgNGb0lDyvPYsOMtNCZxpEQh WDM4nPc/fWmRQv2GE24VPDxTa+KF8QASuK4SK3aHgixIAUiugOpFr5ZuyX7nWhcb 5lPXOfuAB7SL5rq9E4kaikhNiJOCDxMORNZmpe9fii+l4clrmqIaryDHOhCGZFvr ppPOxLFPY1pnFBUtEjrM8TnpVMdV6jB4im7lKfOse/1BhzRQQ/dJg4EFk1S9pHse ryc0wJMM685dgAmELRigWx2IpU7Ma/09WlcCG7AmToeXPcEOa0xFS3CNEat8+bv+ BaWSJ31+se8tDTjjEfV3M23OdUmyv29aTb74wd33u6+jsliFK5psZKbcg3hGCA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDHzCCAgegAwIBAgIJAKs91lAuGzzQMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMxMTUxMjU3WhcNMjUwMzMw MTUxMjU3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2Aw XjAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCAQYwHwYDVR0RBBgwFoYUc3BpZmZlOi8vZXhhbXBsZS5vcmcw DQYJKoZIhvcNAQENBQADggEBAHO2gZiRBlhbCsBSbpaAujVauM4IHhEtoZP1b+cu 22bAm21PTC3T4XAfDj286PrdOYL91FNmbpvR2hL+MrMKH+SBFQCnvNE8FYnMkNRx ysJItGTHdB2yXlBZbpZeRoOCL5oiS5vpQebHU6+AD6hsyM+9rSu4z6gwM2xFLQYQ Dc2IIV8LoB6s/9BB5rLcjVkjdhOR7spsAgFdK6ZYU0K8FoEPvBeEY7CORzenAbhI 8ExVH/3aNB2dfhIu4gFVGVnBh+UQ43YPR6Qs/ON5CS91xpUI0U0tJZgqMfD6kn3A 2U9PgiZyKoKrt+AyLe9OTg1Kb6QoxEFOeCljNp5+J+YPVjU= -----END CERTIFICATE----- wrong-intermediate-no-key-cert-sign.pem000066400000000000000000000044011474173014300334170ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAPKy5MTuxkq7MA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTkwMDM3WhcNMjUwMzI5 MTkwMDM3WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCB4AwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAsgNGb0lDyvPYsOMtNCZxpEQh WDM4nPc/fWmRQv2GE24VPDxTa+KF8QASuK4SK3aHgixIAUiugOpFr5ZuyX7nWhcb 5lPXOfuAB7SL5rq9E4kaikhNiJOCDxMORNZmpe9fii+l4clrmqIaryDHOhCGZFvr ppPOxLFPY1pnFBUtEjrM8TnpVMdV6jB4im7lKfOse/1BhzRQQ/dJg4EFk1S9pHse ryc0wJMM685dgAmELRigWx2IpU7Ma/09WlcCG7AmToeXPcEOa0xFS3CNEat8+bv+ BaWSJ31+se8tDTjjEfV3M23OdUmyv29aTb74wd33u6+jsliFK5psZKbcg3hGCA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDIjCCAgqgAwIBAgIJAIzsgdc6VayiMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMxMTUxNjU0WhcNMjUwMzMw MTUxNjU0WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2Mw YTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCAQIwHwYDVR0RBBgwFoYUc3BpZmZlOi8vZXhhbXBsZS5v cmcwDQYJKoZIhvcNAQENBQADggEBAHzixgdMjrXVEtBfLNTdLrdiBLjlNRcmLbgq 6yBZInw4UieIhj5wnkYlFiZAt3l+v4BxHHBVpW0FFEgXJSUBcyHSiqIY4myiNpGW GsU7rsy3XdmM37y8vc69lhGpaKjDqyN5NWBaPS1N6ZXZAfrCgbzzA20lo9Kebo0/ rItx6r+Q7TQbIdPUwWv5vIms25JODwtmiXxZ3GTOdc79pb396HO0azm4mEPSeadk dYrgeviEsManUlhlGfKREONAMkY1DG5cRoyWLFKSvgFtGu24qLsg2CSReM4nFhcf T8+sSz+RFBwJkb1pfra4IGdIrJtWAQY9TYnVzeV3xroqtQDbeS4= -----END CERTIFICATE-----golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-leaf-ca-true.pem000066400000000000000000000022141474173014300302000ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDLTCCAhWgAwIBAgIJAIATiUHEG6XPMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTg1MDAzWhcNMjUwMzI5 MTg1MDAzWjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo24w bDAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCB4AwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5v cmcvd29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAPWpG81EVmPVbb99qEVnc yxPVy524isxJkx2OAOUXcBmYlZEKa6qEnZh02Qs+8nuBIH6cSwrFqLupycwbW9wo 32fe2Y+UuVEG7b9+vjFofzeVePx0dX5gk9WTb0b+OesmnF/m9uJFZ8ZDw/DFKO1y 0gcqJKPl3HHAz5s83mnBwiugy/G22iorO7iJBkChQrAxdpN2hvsEo88kn1wmopLq kFSeMVc3vrLI9/FL2WnNOYUKbM1J/xrMQFPgs2y9Md/ZZfBQMTQSn1MyRFhLNfq7 vFH1bKDl8R1F6c8n/cU1ZTS84kWpCg5bkpJTKLESbgUj4I6ULxREeNIvNddyU6Qf AA== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-leaf-cert-sign.pem000066400000000000000000000022071474173014300305350ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAP/AfgtzAqe1MA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTk1ODQxWhcNMjUwMzI5 MTk1ODQxWjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCAoQwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAsBgFKbbIo4hJMcgcRqLstLUU rzuyePs9Q7jIMr0dnriy++c+3DfNLQj3jnnhTaTMWoFyLL1tab6xanhxE0+/TkU6 h4Z8hIsFcvZ0GJQ4MFxYpo4i7rly2OfbGXVKCc9Ho5hOcOhiwCpcbRxljVfdlK1x U03VBhgGN4yTxBZv7f9RhqiUo9KTWW1LaTwV0e/B+plMiXWZsDiILndM/1YVvnFU FIvABFDquhkIzN65WMBpUm1U8sjrgGdpAQxJw//dyb59A/HtfAM8tkLiZ4Teuh43 gt88H1PK/f8ksL5rHZcCcvzoq6Dfzfu6QjWNAJg7d6nv3kOd2G+dAGEau3SmPQ== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-leaf-crl-sign.pem000066400000000000000000000022071474173014300303600ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAMhI+pO57LUFMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMjAwNDEzWhcNMjUwMzI5 MjAwNDEzWjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCAYIwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAS/j2y8U79cAY6WxVvd3TgPSy Uh56MbuS9pVKnRMLtwlRK8HoiMeqVYFXDu0xjz+7inVq6xtsr3SV8vy4uYLEr+SA qqQbw3rEWxph6oahFNkc9LOw9c3RHA8cH6izWYtQFsG2TxtMR3fvCQx7x/hxeTD0 xfJEd4LPHfoiVFAtFn1CmglShNp0DA9Y+83s7QfBMfwCc7ih0d79903gpY9o5IVU SNk6Dd7trgkkoEN7P8pq5Rqx4M8XKv4Q1w9lAbL1wQJceM3ANtPhxANXazOhYLfS jH1L7u4I/Kp61hRcdqux/2lNGimka1b0W6TmAiGEu1m2AvIE2sy2P9+L3UfeSA== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-leaf-empty-id.pem000066400000000000000000000021531474173014300303720ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDFjCCAf6gAwIBAgIJAOMjYtQS4O0MMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMxMTI0NjI2WhcNMjUwMzMw MTI0NjI2WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo1cw VTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCB4AwFgYDVR0RBA8wDYILZXhhbXBsZS5vcmcwDQYJKoZIhvcN AQENBQADggEBADJgmurhe2YuHULCYsmDPNQO1hJ79tzVwSZmHo9vLejv3zx7w25z 983CiqbeMRWHh+9gfXsdcwJnn4AQU2VfdYboXchBcA8tK4w8Bpev6DvOketrZ/KR l7LrTv2VV4+eigdXl5dSAQ/4mnLoICvksrKZllxCZWXyay3ctwIa75HCc4xYKGXT WuDNF7jSb21gB4K38pugwQhkH1eMdYvu2zAsBN0ClU7VEEsu1NEXO91+74CHCfDO e/+MPfqspoYrPrDsvkVGjYKxG3IxwsV3XdqB0ofvjoEv8ZqpPmrrfVYc11WBXFIY musYpNAVVBBkn1ILoeJarftnZCZU6VG18mo= -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/testdata/wrong-leaf-no-digital-signature.pem000066400000000000000000000022071474173014300326700ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDKjCCAhKgAwIBAgIJAK/ds1rcEPTnMA0GCSqGSIb3DQEBDQUAMB4xCzAJBgNV BAYTAlVTMQ8wDQYDVQQKDAZTUElGRkUwHhcNMjAwMzMwMTk1MzU1WhcNMjUwMzI5 MTk1MzU1WjAeMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGU1BJRkZFMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvudksUDCwrAb5Ag5zbRaMv0OWcehfN/1 ejrDJUu0gmnYGOrs0xdMPCIacHkOEm0nSJ2b35REBbw13FTCHHaFH9tC0EZrz+Fk mhoP2HTcGRsiibfBvRpyd346sP6JqocpTNljhQxbhKCy4WkAS/KKjrD8NdkQ8zdM h8vBzEkzDwjbjbn3rWgvHcBOU3Zu2mn5/LP4B8oXlGM4n7ZMWK35s+yXKzmwkltp QtVVTmtkCmTLfckBsSgBESnRgIjY/jfwJqrugvPdmx0ToINljiahz0Uci3hRLhYc 3zAsQJoFXaNrm6qFCtOR/xHK/RCKY9bHKEvavVx+F2Wvx3MphddztQIDAQABo2sw aTAdBgNVHQ4EFgQUy4VolgQJBL0rLYyI/JQHlBB0whEwDAYDVR0TAQH/BAIwADAO BgNVHQ8BAf8EBAMCAQYwKgYDVR0RBCMwIYYfc3BpZmZlOi8vZXhhbXBsZS5vcmcv d29ya2xvYWQtMTANBgkqhkiG9w0BAQ0FAAOCAQEAlYTex0bigAfH4BPtLiGXUsDB Tw8p1ztElGHxBHyHs+WAC2Tm2Mlnpxa5e7457WQnta93IuzoU6Ws+TkPFy7IhA8z kjT4LYJGnDGOZ5te6epMpv+1Dul1aVwLTpm3oXmTBRtw2fGubexhj6UVFr9/dqae OihD5OmOTpMzs40SGCibqsKWEUIFtRjtN91kzzbwgAGLbHWrgBmNEimGZ8ASTWs7 PdtnPGqdfKBGT1oHSlnDj3yCeNf9j5isirp2vtaYxVBz1P2wMrUdCKScOc23m2Sm tS2EsyMmLP3GncYiSjDXT4rtNr/NKG3n6g/Km5ZhPWDXtwOz0ur6lovKQyz+wg== -----END CERTIFICATE----- golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/verify.go000066400000000000000000000070521474173014300241250ustar00rootroot00000000000000package x509svid import ( "crypto/x509" "time" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/x509util" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) var x509svidErr = errs.Class("x509svid") // VerifyOption is an option used when verifying X509-SVIDs. type VerifyOption interface { apply(config *verifyConfig) } // WithTime sets the time used when verifying validity periods on the X509-SVID. // If not used, the current time will be used. func WithTime(now time.Time) VerifyOption { return verifyOption(func(config *verifyConfig) { config.now = now }) } // Verify verifies an X509-SVID chain using the X.509 bundle source. It // returns the SPIFFE ID of the X509-SVID and one or more chains back to a root // in the bundle. func Verify(certs []*x509.Certificate, bundleSource x509bundle.Source, opts ...VerifyOption) (spiffeid.ID, [][]*x509.Certificate, error) { config := &verifyConfig{} for _, opt := range opts { opt.apply(config) } switch { case len(certs) == 0: return spiffeid.ID{}, nil, x509svidErr.New("empty certificates chain") case bundleSource == nil: return spiffeid.ID{}, nil, x509svidErr.New("bundleSource is required") } leaf := certs[0] id, err := IDFromCert(leaf) if err != nil { return spiffeid.ID{}, nil, x509svidErr.New("could not get leaf SPIFFE ID: %w", err) } switch { case leaf.IsCA: return id, nil, x509svidErr.New("leaf certificate with CA flag set to true") case leaf.KeyUsage&x509.KeyUsageCertSign > 0: return id, nil, x509svidErr.New("leaf certificate with KeyCertSign key usage") case leaf.KeyUsage&x509.KeyUsageCRLSign > 0: return id, nil, x509svidErr.New("leaf certificate with KeyCrlSign key usage") } bundle, err := bundleSource.GetX509BundleForTrustDomain(id.TrustDomain()) if err != nil { return id, nil, x509svidErr.New("could not get X509 bundle: %w", err) } verifiedChains, err := leaf.Verify(x509.VerifyOptions{ Roots: x509util.NewCertPool(bundle.X509Authorities()), Intermediates: x509util.NewCertPool(certs[1:]), KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, CurrentTime: config.now, }) if err != nil { return id, nil, x509svidErr.New("could not verify leaf certificate: %w", err) } return id, verifiedChains, nil } // ParseAndVerify parses and verifies an X509-SVID chain using the X.509 // bundle source. It returns the SPIFFE ID of the X509-SVID and one or more // chains back to a root in the bundle. func ParseAndVerify(rawCerts [][]byte, bundleSource x509bundle.Source, opts ...VerifyOption) (spiffeid.ID, [][]*x509.Certificate, error) { var certs []*x509.Certificate for _, rawCert := range rawCerts { cert, err := x509.ParseCertificate(rawCert) if err != nil { return spiffeid.ID{}, nil, x509svidErr.New("unable to parse certificate: %w", err) } certs = append(certs, cert) } return Verify(certs, bundleSource, opts...) } // IDFromCert extracts the SPIFFE ID from the URI SAN of the provided // certificate. It will return an an error if the certificate does not have // exactly one URI SAN with a well-formed SPIFFE ID. func IDFromCert(cert *x509.Certificate) (spiffeid.ID, error) { switch { case len(cert.URIs) == 0: return spiffeid.ID{}, errs.New("certificate contains no URI SAN") case len(cert.URIs) > 1: return spiffeid.ID{}, errs.New("certificate contains more than one URI SAN") } return spiffeid.FromURI(cert.URIs[0]) } type verifyConfig struct { now time.Time } type verifyOption func(config *verifyConfig) func (fn verifyOption) apply(config *verifyConfig) { fn(config) } golang-github-spiffe-go-spiffe-2.4.0/v2/svid/x509svid/verify_test.go000066400000000000000000000125451474173014300251670ustar00rootroot00000000000000package x509svid_test import ( "crypto/x509" "net/url" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/require" ) func TestVerify(t *testing.T) { td1 := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td1) leaf1 := ca1.CreateX509SVID(spiffeid.RequireFromPath(td1, "/workload")).Certificates leaf1NoURI := removeURIs(leaf1[0]) leaf1DupUris := dupURIs(leaf1[0]) leaf1IsCA := setIsCA(leaf1[0]) leaf1WithCertSign := appendKeyUsage(leaf1[0], x509.KeyUsageCertSign) leaf1WithCRLSign := appendKeyUsage(leaf1[0], x509.KeyUsageCRLSign) bundle1 := ca1.X509Bundle() td2 := spiffeid.RequireTrustDomainFromString("spiffe://domain2.test") ca2 := test.NewCA(t, td2) bundle2 := ca2.X509Bundle() // bad leaf cert... invalid spiffe ID leafBad, _ := ca1.CreateX509Certificate(test.WithURIs(&url.URL{Scheme: "sparfe", Host: "domain1.test", Path: "/workload"})) // bad set of roots... sets roots for ca2 under domain1.test bundleBad := spiffebundle.FromX509Authorities(td1, bundle2.X509Authorities()) testCases := []struct { name string chain []*x509.Certificate bundle x509bundle.Source opts []x509svid.VerifyOption expectedID spiffeid.ID err string }{ { name: "empty chain", bundle: bundle1, err: "x509svid: empty certificates chain", }, { name: "empty bundle", chain: leaf1, err: "x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: \"domain1.test\"", bundle: &x509bundle.Bundle{}, }, { name: "nil bundle", chain: leaf1, err: "x509svid: bundleSource is required", }, { name: "no roots", chain: leaf1, err: "x509svid: could not verify leaf certificate: x509: certificate signed by unknown authority", bundle: x509bundle.New(spiffeid.RequireTrustDomainFromString("domain1.test")), }, { name: "no roots for leaf cert domain", chain: leaf1, bundle: bundle2, err: `x509svid: could not get X509 bundle: x509bundle: no X.509 bundle found for trust domain: "domain1.test"`, }, { name: "bad leaf cert id", chain: leafBad, bundle: bundle1, err: `x509svid: could not get leaf SPIFFE ID: scheme is missing or invalid`, }, { name: "verification fails", chain: leaf1, bundle: bundleBad, err: "x509svid: could not verify leaf certificate: x509: certificate signed by unknown authority", }, { name: "no URI SAN", chain: leaf1NoURI, bundle: bundle1, err: "x509svid: could not get leaf SPIFFE ID: certificate contains no URI SAN", }, { name: "more than one URI SAN", chain: leaf1DupUris, bundle: bundle1, err: "x509svid: could not get leaf SPIFFE ID: certificate contains more than one URI SAN", }, { name: "leaf is CA", chain: leaf1IsCA, bundle: bundle1, err: "x509svid: leaf certificate with CA flag set to true", }, { name: "leaf has KeyUsageCertSign", chain: leaf1WithCertSign, bundle: bundle1, err: "x509svid: leaf certificate with KeyCertSign key usage", }, { name: "leaf has KeyUsageCRLSign", chain: leaf1WithCRLSign, bundle: bundle1, err: "x509svid: leaf certificate with KeyCrlSign key usage", }, { name: "with time", chain: leaf1, bundle: bundle1, opts: []x509svid.VerifyOption{x509svid.WithTime(leaf1[0].NotAfter.Add(time.Second))}, err: "x509svid: could not verify leaf certificate: x509: certificate has expired", }, { name: "success", chain: leaf1, bundle: bundle1, }, } for _, testCase := range testCases { testCase := testCase // alias loop var as it is used in the closure t.Run(testCase.name, func(t *testing.T) { _, verifiedChains, err := x509svid.Verify(testCase.chain, testCase.bundle, testCase.opts...) if testCase.err != "" { require.Error(t, err) require.Contains(t, err.Error(), testCase.err) return } require.NoError(t, err) require.NotNil(t, verifiedChains) }) } } func TestParseAndVerify(t *testing.T) { td1 := spiffeid.RequireTrustDomainFromString("domain1.test") ca1 := test.NewCA(t, td1) leaf1 := ca1.CreateX509SVID(spiffeid.RequireFromPath(td1, "/workload")).Certificates bundle1 := ca1.X509Bundle() rawLeaf := leaf1[0].Raw _, verifiedChains, err := x509svid.ParseAndVerify([][]byte{rawLeaf}, bundle1) require.NoError(t, err) require.NotNil(t, verifiedChains) // We modify some byte to make the parsing fail. rawLeaf[0] = 0x27 _, verifiedChains, err = x509svid.ParseAndVerify([][]byte{rawLeaf}, bundle1) require.Contains(t, err.Error(), "x509svid: unable to parse certificate") require.Nil(t, verifiedChains) } func removeURIs(cert *x509.Certificate) []*x509.Certificate { c := *cert c.URIs = []*url.URL{} return []*x509.Certificate{&c} } func dupURIs(cert *x509.Certificate) []*x509.Certificate { c := *cert c.URIs = append(c.URIs, c.URIs...) return []*x509.Certificate{&c} } func setIsCA(cert *x509.Certificate) []*x509.Certificate { c := *cert c.IsCA = true return []*x509.Certificate{&c} } func appendKeyUsage(cert *x509.Certificate, ku x509.KeyUsage) []*x509.Certificate { c := *cert c.KeyUsage |= ku return []*x509.Certificate{&c} } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/000077500000000000000000000000001474173014300222425ustar00rootroot00000000000000golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr.go000066400000000000000000000037711474173014300235130ustar00rootroot00000000000000package workloadapi import ( "errors" "net" "net/url" "os" ) const ( // SocketEnv is the environment variable holding the default Workload API // address. SocketEnv = "SPIFFE_ENDPOINT_SOCKET" ) func GetDefaultAddress() (string, bool) { return os.LookupEnv(SocketEnv) } // ValidateAddress validates that the provided address // can be parsed to a gRPC target string for dialing // a Workload API endpoint exposed as either a Unix // Domain Socket or TCP socket. func ValidateAddress(addr string) error { _, err := parseTargetFromStringAddr(addr) return err } // parseTargetFromStringAddr parses the endpoint address and returns a gRPC target // string for dialing. func parseTargetFromStringAddr(addr string) (string, error) { u, err := url.Parse(addr) if err != nil { return "", errors.New("workload endpoint socket is not a valid URI: " + err.Error()) } return parseTargetFromURLAddr(u) } func parseTargetFromURLAddr(u *url.URL) (string, error) { if u.Scheme == "tcp" { switch { case u.Opaque != "": return "", errors.New("workload endpoint tcp socket URI must not be opaque") case u.User != nil: return "", errors.New("workload endpoint tcp socket URI must not include user info") case u.Host == "": return "", errors.New("workload endpoint tcp socket URI must include a host") case u.Path != "": return "", errors.New("workload endpoint tcp socket URI must not include a path") case u.RawQuery != "": return "", errors.New("workload endpoint tcp socket URI must not include query values") case u.Fragment != "": return "", errors.New("workload endpoint tcp socket URI must not include a fragment") } ip := net.ParseIP(u.Hostname()) if ip == nil { return "", errors.New("workload endpoint tcp socket URI host component must be an IP:port") } port := u.Port() if port == "" { return "", errors.New("workload endpoint tcp socket URI host component must include a port") } return net.JoinHostPort(ip.String(), port), nil } return parseTargetFromURLAddrOS(u) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr_posix.go000066400000000000000000000017051474173014300247300ustar00rootroot00000000000000//go:build !windows // +build !windows package workloadapi import ( "errors" "net/url" ) var ( ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"unix\" scheme") ) func parseTargetFromURLAddrOS(u *url.URL) (string, error) { switch u.Scheme { case "unix": switch { case u.Opaque != "": return "", errors.New("workload endpoint unix socket URI must not be opaque") case u.User != nil: return "", errors.New("workload endpoint unix socket URI must not include user info") case u.Host == "" && u.Path == "": return "", errors.New("workload endpoint unix socket URI must include a path") case u.RawQuery != "": return "", errors.New("workload endpoint unix socket URI must not include query values") case u.Fragment != "": return "", errors.New("workload endpoint unix socket URI must not include a fragment") } return u.String(), nil default: return "", ErrInvalidEndpointScheme } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr_posix_test.go000066400000000000000000000013441474173014300257660ustar00rootroot00000000000000//go:build !windows // +build !windows package workloadapi func validateAddressCasesOS() []validateAddressCase { return []validateAddressCase{ { addr: "unix:opaque", err: "workload endpoint unix socket URI must not be opaque", }, { addr: "unix://", err: "workload endpoint unix socket URI must include a path", }, { addr: "unix://foo?whatever", err: "workload endpoint unix socket URI must not include query values", }, { addr: "unix://foo#whatever", err: "workload endpoint unix socket URI must not include a fragment", }, { addr: "unix://john:doe@foo/path", err: "workload endpoint unix socket URI must not include user info", }, { addr: "unix://foo", err: "", }, } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr_test.go000066400000000000000000000037671474173014300245570ustar00rootroot00000000000000package workloadapi import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type validateAddressCase struct { addr string err string } func TestGetDefaultAddress(t *testing.T) { if orig, ok := os.LookupEnv(SocketEnv); ok { defer os.Setenv(SocketEnv, orig) } else { defer os.Unsetenv(SocketEnv) } os.Unsetenv(SocketEnv) addr, ok := GetDefaultAddress() assert.False(t, ok) assert.Equal(t, "", addr) os.Setenv(SocketEnv, "ADDRESS") addr, ok = GetDefaultAddress() assert.True(t, ok) assert.Equal(t, "ADDRESS", addr) } func TestValidateAddress(t *testing.T) { testCases := []validateAddressCase{ { addr: "\t", err: "net/url: invalid control character in URL", }, { addr: "blah", err: ErrInvalidEndpointScheme.Error(), }, { addr: "tcp:opaque", err: "workload endpoint tcp socket URI must not be opaque", }, { addr: "tcp://", err: "workload endpoint tcp socket URI must include a host", }, { addr: "tcp://1.2.3.4:5?whatever", err: "workload endpoint tcp socket URI must not include query values", }, { addr: "tcp://1.2.3.4:5#whatever", err: "workload endpoint tcp socket URI must not include a fragment", }, { addr: "tcp://john:doe@1.2.3.4:5/path", err: "workload endpoint tcp socket URI must not include user info", }, { addr: "tcp://1.2.3.4:5/path", err: "workload endpoint tcp socket URI must not include a path", }, { addr: "tcp://foo", err: "workload endpoint tcp socket URI host component must be an IP:port", }, { addr: "tcp://1.2.3.4", err: "workload endpoint tcp socket URI host component must include a port", }, { addr: "tcp://1.2.3.4:5", err: "", }, } testCases = append(testCases, validateAddressCasesOS()...) for _, testCase := range testCases { err := ValidateAddress(testCase.addr) if testCase.err != "" { require.Error(t, err) assert.Contains(t, err.Error(), testCase.err) continue } assert.NoError(t, err) } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr_windows.go000066400000000000000000000015501474173014300252560ustar00rootroot00000000000000//go:build windows // +build windows package workloadapi import ( "errors" "net/url" ) var ( ErrInvalidEndpointScheme = errors.New("workload endpoint socket URI must have a \"tcp\" or \"npipe\" scheme") ) func parseTargetFromURLAddrOS(u *url.URL) (string, error) { switch u.Scheme { case "npipe": switch { case u.Opaque == "" && u.Host != "": return "", errors.New("workload endpoint named pipe URI must be opaque") case u.Opaque == "": return "", errors.New("workload endpoint named pipe URI must include an opaque part") case u.RawQuery != "": return "", errors.New("workload endpoint named pipe URI must not include query values") case u.Fragment != "": return "", errors.New("workload endpoint named pipe URI must not include a fragment") } return namedPipeTarget(u.Opaque), nil default: return "", ErrInvalidEndpointScheme } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/addr_windows_test.go000066400000000000000000000013301474173014300263110ustar00rootroot00000000000000//go:build windows // +build windows package workloadapi func validateAddressCasesOS() []validateAddressCase { return []validateAddressCase{ { addr: "npipe:pipeName", err: "", }, { addr: "npipe:pipe/name", err: "", }, { addr: "npipe:pipe\\name", err: "", }, { addr: "npipe:", err: "workload endpoint named pipe URI must include an opaque part", }, { addr: "npipe://foo", err: "workload endpoint named pipe URI must be opaque", }, { addr: "npipe:pipeName?query", err: "workload endpoint named pipe URI must not include query values", }, { addr: "npipe:pipeName#fragment", err: "workload endpoint named pipe URI must not include a fragment", }, } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/backoff.go000066400000000000000000000022201474173014300241600ustar00rootroot00000000000000package workloadapi import ( "math" "time" ) // BackoffStrategy provides backoff facilities. type BackoffStrategy interface { // NewBackoff returns a new backoff for the strategy. The returned // Backoff is in the same state that it would be in after a call to // Reset(). NewBackoff() Backoff } // Backoff provides backoff for a workload API operation. type Backoff interface { // Next returns the next backoff period. Next() time.Duration // Reset() resets the backoff. Reset() } type defaultBackoffStrategy struct{} func (defaultBackoffStrategy) NewBackoff() Backoff { return newLinearBackoff() } // linearBackoff defines an linear backoff policy. type linearBackoff struct { initialDelay time.Duration maxDelay time.Duration n int } func newLinearBackoff() *linearBackoff { return &linearBackoff{ initialDelay: time.Second, maxDelay: 30 * time.Second, n: 0, } } func (b *linearBackoff) Next() time.Duration { backoff := float64(b.n) + 1 d := math.Min(b.initialDelay.Seconds()*backoff, b.maxDelay.Seconds()) b.n++ return time.Duration(d) * time.Second } func (b *linearBackoff) Reset() { b.n = 0 } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/backoff_test.go000066400000000000000000000012401474173014300252200ustar00rootroot00000000000000package workloadapi import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestLinearBackoff(t *testing.T) { testUntilMax := func(t *testing.T, b *linearBackoff) { for i := 1; i < 30; i++ { require.Equal(t, time.Duration(i)*time.Second, b.Next()) } require.Equal(t, 30*time.Second, b.Next()) require.Equal(t, 30*time.Second, b.Next()) require.Equal(t, 30*time.Second, b.Next()) } t.Run("test max", func(t *testing.T) { t.Parallel() b := newLinearBackoff() testUntilMax(t, b) }) t.Run("test reset", func(t *testing.T) { t.Parallel() b := newLinearBackoff() testUntilMax(t, b) b.Reset() testUntilMax(t, b) }) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/bundlesource.go000066400000000000000000000137641474173014300252760ustar00rootroot00000000000000package workloadapi import ( "context" "crypto" "crypto/x509" "sync" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/zeebo/errs" ) var bundlesourceErr = errs.Class("bundlesource") // BundleSource is a source of SPIFFE bundles maintained via the Workload API. type BundleSource struct { watcher *watcher mtx sync.RWMutex x509Authorities map[spiffeid.TrustDomain][]*x509.Certificate jwtAuthorities map[spiffeid.TrustDomain]map[string]crypto.PublicKey closeMtx sync.RWMutex closed bool } // NewBundleSource creates a new BundleSource. It blocks until the initial // update has been received from the Workload API. The source should be closed // when no longer in use to free underlying resources. func NewBundleSource(ctx context.Context, options ...BundleSourceOption) (_ *BundleSource, err error) { config := &bundleSourceConfig{} for _, option := range options { option.configureBundleSource(config) } s := &BundleSource{ x509Authorities: make(map[spiffeid.TrustDomain][]*x509.Certificate), jwtAuthorities: make(map[spiffeid.TrustDomain]map[string]crypto.PublicKey), } s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, s.setJWTBundles) if err != nil { return nil, err } return s, nil } // Close closes the source, dropping the connection to the Workload API. // Other source methods will return an error after Close has been called. // The underlying Workload API client will also be closed if it is owned by // the BundleSource (i.e. not provided via the WithClient option). func (s *BundleSource) Close() error { s.closeMtx.Lock() s.closed = true s.closeMtx.Unlock() return s.watcher.Close() } // GetBundleForTrustDomain returns the SPIFFE bundle for the given trust // domain. It implements the spiffebundle.Source interface. func (s *BundleSource) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*spiffebundle.Bundle, error) { if err := s.checkClosed(); err != nil { return nil, err } s.mtx.RLock() defer s.mtx.RUnlock() x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] if !hasX509Authorities && !hasJWTAuthorities { return nil, bundlesourceErr.New("no SPIFFE bundle for trust domain %q", trustDomain) } bundle := spiffebundle.New(trustDomain) if hasX509Authorities { bundle.SetX509Authorities(x509Authorities) } if hasJWTAuthorities { bundle.SetJWTAuthorities(jwtAuthorities) } return bundle, nil } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the x509bundle.Source interface. func (s *BundleSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { if err := s.checkClosed(); err != nil { return nil, err } s.mtx.RLock() defer s.mtx.RUnlock() x509Authorities, hasX509Authorities := s.x509Authorities[trustDomain] if !hasX509Authorities { return nil, bundlesourceErr.New("no X.509 bundle for trust domain %q", trustDomain) } return x509bundle.FromX509Authorities(trustDomain, x509Authorities), nil } // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. It implements the jwtbundle.Source interface. func (s *BundleSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { if err := s.checkClosed(); err != nil { return nil, err } s.mtx.RLock() defer s.mtx.RUnlock() jwtAuthorities, hasJWTAuthorities := s.jwtAuthorities[trustDomain] if !hasJWTAuthorities { return nil, bundlesourceErr.New("no JWT bundle for trust domain %q", trustDomain) } return jwtbundle.FromJWTAuthorities(trustDomain, jwtAuthorities), nil } // WaitUntilUpdated waits until the source is updated or the context is done, // in which case ctx.Err() is returned. func (s *BundleSource) WaitUntilUpdated(ctx context.Context) error { return s.watcher.WaitUntilUpdated(ctx) } // Updated returns a channel that is sent on whenever the source is updated. func (s *BundleSource) Updated() <-chan struct{} { return s.watcher.Updated() } func (s *BundleSource) setX509Context(x509Context *X509Context) { s.mtx.Lock() defer s.mtx.Unlock() newBundles := x509Context.Bundles.Bundles() // Add/replace the X.509 authorities from the X.509 context. Track the trust // domains represented in the new X.509 context so we can determine which // existing trust domains are no longer represented. trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) for _, newBundle := range newBundles { trustDomains[newBundle.TrustDomain()] = struct{}{} s.x509Authorities[newBundle.TrustDomain()] = newBundle.X509Authorities() } // Remove the X.509 authority entries for trust domains no longer // represented in the X.509 context. for existingTD := range s.x509Authorities { if _, ok := trustDomains[existingTD]; ok { continue } delete(s.x509Authorities, existingTD) } } func (s *BundleSource) setJWTBundles(bundles *jwtbundle.Set) { s.mtx.Lock() defer s.mtx.Unlock() newBundles := bundles.Bundles() // Add/replace the JWT authorities from the JWT bundles. Track the trust // domains represented in the new JWT bundles so we can determine which // existing trust domains are no longer represented. trustDomains := make(map[spiffeid.TrustDomain]struct{}, len(newBundles)) for _, newBundle := range newBundles { trustDomains[newBundle.TrustDomain()] = struct{}{} s.jwtAuthorities[newBundle.TrustDomain()] = newBundle.JWTAuthorities() } // Remove the JWT authority entries for trust domains no longer represented // in the JWT bundles. for existingTD := range s.jwtAuthorities { if _, ok := trustDomains[existingTD]; ok { continue } delete(s.jwtAuthorities, existingTD) } } func (s *BundleSource) checkClosed() error { s.closeMtx.RLock() defer s.closeMtx.RUnlock() if s.closed { return bundlesourceErr.New("source is closed") } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/bundlesource_test.go000066400000000000000000000175111474173014300263270ustar00rootroot00000000000000package workloadapi_test import ( "context" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBundleSourceDoesNotReturnUntilInitialUpdate(t *testing.T) { api := fakeworkloadapi.New(t) defer api.Stop() // Using a timeout here to detect that it doesn't return isn't ideal. Not // sure how to deterministically test it otherwise. ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() // Create the source. It will wait for the initial response. source, err := workloadapi.NewBundleSource(ctx, withAddr(api)) if !assert.EqualError(t, err, context.DeadlineExceeded.Error()) { source.Close() } } func TestBundleSourceFailsCallsIfClosed(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) // Set the initial response, containing both X.509 and JWT materials. svid := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload")) api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{svid}, Bundle: ca.X509Bundle(), }) api.SetJWTBundles(ca.JWTBundle()) // Create the source. It will wait for the initial response. source, err := workloadapi.NewBundleSource(ctx, withAddr(api)) require.NoError(t, err) // Close the source require.NoError(t, source.Close()) _, err = source.GetBundleForTrustDomain(td) require.EqualError(t, err, "bundlesource: source is closed") _, err = source.GetX509BundleForTrustDomain(td) require.EqualError(t, err, "bundlesource: source is closed") _, err = source.GetJWTBundleForTrustDomain(td) require.EqualError(t, err, "bundlesource: source is closed") } func TestBundleSourceGetsUpdates(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() // Set up the CA for a few trust domains domain1TD := spiffeid.RequireTrustDomainFromString("domain1.test") domain1CA := test.NewCA(t, domain1TD) domain1Bundle := domain1CA.Bundle() domain1X509Bundle := domain1CA.X509Bundle() domain1JWTBundle := domain1CA.JWTBundle() domain2TD := spiffeid.RequireTrustDomainFromString("domain2.test") domain2CA := test.NewCA(t, domain2TD) domain2Bundle := domain2CA.Bundle() domain2X509Bundle := domain2CA.X509Bundle() domain2JWTBundle := domain2CA.JWTBundle() svids := []*x509svid.SVID{ domain1CA.CreateX509SVID(spiffeid.RequireFromPath(domain1TD, "/workload")), } // Set the initial response api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: svids, Bundle: domain1X509Bundle, FederatedBundles: []*x509bundle.Bundle{domain2X509Bundle}, }) api.SetJWTBundles(domain1JWTBundle, domain2JWTBundle) // Create the source. It will wait for the initial response. source, sourceDone := newBundleSource(ctx, t, api) defer sourceDone() // Assert expected bundle contents. requireBundle(t, source, domain1TD, domain1Bundle) requireX509Bundle(t, source, domain1TD, domain1X509Bundle) requireJWTBundle(t, source, domain1TD, domain1JWTBundle) requireBundle(t, source, domain2TD, domain2Bundle) requireX509Bundle(t, source, domain2TD, domain2X509Bundle) requireJWTBundle(t, source, domain2TD, domain2JWTBundle) // Now send an update to both the X.509 context and JWT bundle streams // that removes the JWT bundles for each trust domain and no longer // provides the X.509 bundle for domain2.test api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: svids, Bundle: domain1X509Bundle, }) require.NoError(t, source.WaitUntilUpdated(ctx)) api.SetJWTBundles() require.NoError(t, source.WaitUntilUpdated(ctx)) // Assert that: // - domain1.test SPIFFE bundle only has the X.509 authorities // - domain1.test X.509 bundle is available // - domain1.test JWT bundle is not available requireBundle(t, source, domain1TD, spiffebundle.FromX509Bundle(domain1X509Bundle)) requireX509Bundle(t, source, domain1TD, domain1X509Bundle) requireNoJWTBundle(t, source, domain1TD, `bundlesource: no JWT bundle for trust domain "domain1.test"`) requireNoBundle(t, source, domain2TD, `bundlesource: no SPIFFE bundle for trust domain "domain2.test"`) requireNoX509Bundle(t, source, domain2TD, `bundlesource: no X.509 bundle for trust domain "domain2.test"`) requireNoJWTBundle(t, source, domain2TD, `bundlesource: no JWT bundle for trust domain "domain2.test"`) } func TestBundleSourceDoesNotReturnX509BundleIfMissingFromX509Response(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() domain1TD := spiffeid.RequireTrustDomainFromString("domain1.test") domain1CA := test.NewCA(t, domain1TD) domain1X509Bundle := domain1CA.X509Bundle() domain2TD := spiffeid.RequireTrustDomainFromString("domain2.test") domain2CA := test.NewCA(t, domain2TD) domain2JWTBundle := domain2CA.JWTBundle() // X509SVIDResponse's are rejected if there is no bundle, so we'll use // two trust domains for the test and just not set an X.509 bundle // for the domain2.test domain. api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{domain1CA.CreateX509SVID(spiffeid.RequireFromPath(domain1TD, "/workload"))}, Bundle: domain1X509Bundle, }) api.SetJWTBundles(domain2JWTBundle) // Create the source. It will wait for the initial response. source, sourceDone := newBundleSource(ctx, t, api) defer sourceDone() // Assert that the domain2.test: // - SPIFFE bundle exists with only JWT authorities // - X.509 bundle does not exist // - JWT bundle exists requireBundle(t, source, domain2TD, spiffebundle.FromJWTBundle(domain2JWTBundle)) requireNoX509Bundle(t, source, domain2TD, `bundlesource: no X.509 bundle for trust domain "domain2.test"`) requireJWTBundle(t, source, domain2TD, domain2JWTBundle) } func TestBundleSourceDoesNotReturnJWTBundleIfMissingFromJWTBundlesResponse(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) x509Bundle := ca.X509Bundle() // Set the initial X509SVID response api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload"))}, Bundle: x509Bundle, }) api.SetJWTBundles() // Create the source. It will wait for the initial response. source, sourceDone := newBundleSource(ctx, t, api) defer sourceDone() // Assert that: // - SPIFFE bundle exists with only JWT authorities // - X.509 bundle does not exist // - JWT bundle exists requireBundle(t, source, td, spiffebundle.FromX509Bundle(x509Bundle)) requireX509Bundle(t, source, td, x509Bundle) requireNoJWTBundle(t, source, td, `bundlesource: no JWT bundle for trust domain "domain.test"`) } func newBundleSource(ctx context.Context, tb testing.TB, api *fakeworkloadapi.WorkloadAPI) (*workloadapi.BundleSource, func()) { source, err := workloadapi.NewBundleSource(ctx, withAddr(api)) require.NoError(tb, err) return source, func() { assert.NoError(tb, source.Close()) } } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/client.go000066400000000000000000000351121474173014300240510ustar00rootroot00000000000000package workloadapi import ( "context" "crypto/x509" "errors" "time" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/logger" "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // Client is a Workload API client. type Client struct { conn *grpc.ClientConn wlClient workload.SpiffeWorkloadAPIClient config clientConfig } // New dials the Workload API and returns a client. The client should be closed // when no longer in use to free underlying resources. func New(ctx context.Context, options ...ClientOption) (*Client, error) { c := &Client{ config: defaultClientConfig(), } for _, opt := range options { opt.configureClient(&c.config) } err := c.setAddress() if err != nil { return nil, err } c.conn, err = c.newConn(ctx) if err != nil { return nil, err } c.wlClient = workload.NewSpiffeWorkloadAPIClient(c.conn) return c, nil } // Close closes the client. func (c *Client) Close() error { return c.conn.Close() } // FetchX509SVID fetches the default X509-SVID, i.e. the first in the list // returned by the Workload API. func (c *Client) FetchX509SVID(ctx context.Context) (*x509svid.SVID, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) if err != nil { return nil, err } resp, err := stream.Recv() if err != nil { return nil, err } svids, err := parseX509SVIDs(resp, true) if err != nil { return nil, err } return svids[0], nil } // FetchX509SVIDs fetches all X509-SVIDs. func (c *Client) FetchX509SVIDs(ctx context.Context) ([]*x509svid.SVID, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) if err != nil { return nil, err } resp, err := stream.Recv() if err != nil { return nil, err } return parseX509SVIDs(resp, false) } // FetchX509Bundles fetches the X.509 bundles. func (c *Client) FetchX509Bundles(ctx context.Context) (*x509bundle.Set, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) if err != nil { return nil, err } resp, err := stream.Recv() if err != nil { return nil, err } return parseX509BundlesResponse(resp) } // WatchX509Bundles watches for changes to the X.509 bundles. The watcher receives // the updated X.509 bundles. func (c *Client) WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher) error { backoff := c.config.backoffStrategy.NewBackoff() for { err := c.watchX509Bundles(ctx, watcher, backoff) watcher.OnX509BundlesWatchError(err) err = c.handleWatchError(ctx, err, backoff) if err != nil { return err } } } // FetchX509Context fetches the X.509 context, which contains both X509-SVIDs // and X.509 bundles. func (c *Client) FetchX509Context(ctx context.Context) (*X509Context, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) if err != nil { return nil, err } resp, err := stream.Recv() if err != nil { return nil, err } return parseX509Context(resp) } // WatchX509Context watches for updates to the X.509 context. The watcher // receives the updated X.509 context. func (c *Client) WatchX509Context(ctx context.Context, watcher X509ContextWatcher) error { backoff := c.config.backoffStrategy.NewBackoff() for { err := c.watchX509Context(ctx, watcher, backoff) watcher.OnX509ContextWatchError(err) err = c.handleWatchError(ctx, err, backoff) if err != nil { return err } } } // FetchJWTSVID fetches a JWT-SVID. func (c *Client) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() audience := append([]string{params.Audience}, params.ExtraAudiences...) resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ SpiffeId: params.Subject.String(), Audience: audience, }) if err != nil { return nil, err } svids, err := parseJWTSVIDs(resp, audience, true) if err != nil { return nil, err } return svids[0], nil } // FetchJWTSVIDs fetches all JWT-SVIDs. func (c *Client) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() audience := append([]string{params.Audience}, params.ExtraAudiences...) resp, err := c.wlClient.FetchJWTSVID(ctx, &workload.JWTSVIDRequest{ SpiffeId: params.Subject.String(), Audience: audience, }) if err != nil { return nil, err } return parseJWTSVIDs(resp, audience, false) } // FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed // by a SPIFFE ID of the trust domain to which they belong. func (c *Client) FetchJWTBundles(ctx context.Context) (*jwtbundle.Set, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) if err != nil { return nil, err } resp, err := stream.Recv() if err != nil { return nil, err } return parseJWTSVIDBundles(resp) } // WatchJWTBundles watches for changes to the JWT bundles. The watcher receives // the updated JWT bundles. func (c *Client) WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher) error { backoff := c.config.backoffStrategy.NewBackoff() for { err := c.watchJWTBundles(ctx, watcher, backoff) watcher.OnJWTBundlesWatchError(err) err = c.handleWatchError(ctx, err, backoff) if err != nil { return err } } } // ValidateJWTSVID validates the JWT-SVID token. The parsed and validated // JWT-SVID is returned. func (c *Client) ValidateJWTSVID(ctx context.Context, token, audience string) (*jwtsvid.SVID, error) { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() _, err := c.wlClient.ValidateJWTSVID(ctx, &workload.ValidateJWTSVIDRequest{ Svid: token, Audience: audience, }) if err != nil { return nil, err } return jwtsvid.ParseInsecure(token, []string{audience}) } func (c *Client) newConn(ctx context.Context) (*grpc.ClientConn, error) { c.config.dialOptions = append(c.config.dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) c.appendDialOptionsOS() return grpc.DialContext(ctx, c.config.address, c.config.dialOptions...) //nolint:staticcheck // preserve backcompat with WithDialOptions option } func (c *Client) handleWatchError(ctx context.Context, err error, backoff Backoff) error { code := status.Code(err) if code == codes.Canceled { return err } if code == codes.InvalidArgument { c.config.log.Errorf("Canceling watch: %v", err) return err } c.config.log.Errorf("Failed to watch the Workload API: %v", err) retryAfter := backoff.Next() c.config.log.Debugf("Retrying watch in %s", retryAfter) select { case <-time.After(retryAfter): return nil case <-ctx.Done(): return ctx.Err() } } func (c *Client) watchX509Context(ctx context.Context, watcher X509ContextWatcher, backoff Backoff) error { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() c.config.log.Debugf("Watching X.509 contexts") stream, err := c.wlClient.FetchX509SVID(ctx, &workload.X509SVIDRequest{}) if err != nil { return err } for { resp, err := stream.Recv() if err != nil { return err } backoff.Reset() x509Context, err := parseX509Context(resp) if err != nil { c.config.log.Errorf("Failed to parse X509-SVID response: %v", err) watcher.OnX509ContextWatchError(err) continue } watcher.OnX509ContextUpdate(x509Context) } } func (c *Client) watchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, backoff Backoff) error { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() c.config.log.Debugf("Watching JWT bundles") stream, err := c.wlClient.FetchJWTBundles(ctx, &workload.JWTBundlesRequest{}) if err != nil { return err } for { resp, err := stream.Recv() if err != nil { return err } backoff.Reset() jwtbundleSet, err := parseJWTSVIDBundles(resp) if err != nil { c.config.log.Errorf("Failed to parse JWT bundle response: %v", err) watcher.OnJWTBundlesWatchError(err) continue } watcher.OnJWTBundlesUpdate(jwtbundleSet) } } func (c *Client) watchX509Bundles(ctx context.Context, watcher X509BundleWatcher, backoff Backoff) error { ctx, cancel := context.WithCancel(withHeader(ctx)) defer cancel() c.config.log.Debugf("Watching X.509 bundles") stream, err := c.wlClient.FetchX509Bundles(ctx, &workload.X509BundlesRequest{}) if err != nil { return err } for { resp, err := stream.Recv() if err != nil { return err } backoff.Reset() x509bundleSet, err := parseX509BundlesResponse(resp) if err != nil { c.config.log.Errorf("Failed to parse X.509 bundle response: %v", err) watcher.OnX509BundlesWatchError(err) continue } watcher.OnX509BundlesUpdate(x509bundleSet) } } // X509ContextWatcher receives X509Context updates from the Workload API. type X509ContextWatcher interface { // OnX509ContextUpdate is called with the latest X.509 context retrieved // from the Workload API. OnX509ContextUpdate(*X509Context) // OnX509ContextWatchError is called when there is a problem establishing // or maintaining connectivity with the Workload API. OnX509ContextWatchError(error) } // JWTBundleWatcher receives JWT bundle updates from the Workload API. type JWTBundleWatcher interface { // OnJWTBundlesUpdate is called with the latest JWT bundle set retrieved // from the Workload API. OnJWTBundlesUpdate(*jwtbundle.Set) // OnJWTBundlesWatchError is called when there is a problem establishing // or maintaining connectivity with the Workload API. OnJWTBundlesWatchError(error) } // X509BundleWatcher receives X.509 bundle updates from the Workload API. type X509BundleWatcher interface { // OnX509BundlesUpdate is called with the latest X.509 bundle set retrieved // from the Workload API. OnX509BundlesUpdate(*x509bundle.Set) // OnX509BundlesWatchError is called when there is a problem establishing // or maintaining connectivity with the Workload API. OnX509BundlesWatchError(error) } func withHeader(ctx context.Context) context.Context { header := metadata.Pairs("workload.spiffe.io", "true") return metadata.NewOutgoingContext(ctx, header) } func defaultClientConfig() clientConfig { return clientConfig{ log: logger.Null, backoffStrategy: defaultBackoffStrategy{}, } } func parseX509Context(resp *workload.X509SVIDResponse) (*X509Context, error) { svids, err := parseX509SVIDs(resp, false) if err != nil { return nil, err } bundles, err := parseX509Bundles(resp) if err != nil { return nil, err } return &X509Context{ SVIDs: svids, Bundles: bundles, }, nil } // parseX509SVIDs parses one or all of the SVIDs in the response. If firstOnly // is true, then only the first SVID in the response is parsed and returned. // Otherwise, all SVIDs are parsed and returned. func parseX509SVIDs(resp *workload.X509SVIDResponse, firstOnly bool) ([]*x509svid.SVID, error) { n := len(resp.Svids) if n == 0 { return nil, errors.New("no SVIDs in response") } if firstOnly { n = 1 } hints := make(map[string]struct{}, n) svids := make([]*x509svid.SVID, 0, n) for i := 0; i < n; i++ { svid := resp.Svids[i] // In the event of more than one X509SVID message with the same hint value set, then the first message in the // list SHOULD be selected. if _, ok := hints[svid.Hint]; ok && svid.Hint != "" { continue } hints[svid.Hint] = struct{}{} s, err := x509svid.ParseRaw(svid.X509Svid, svid.X509SvidKey) if err != nil { return nil, err } s.Hint = svid.Hint svids = append(svids, s) } return svids, nil } func parseX509Bundles(resp *workload.X509SVIDResponse) (*x509bundle.Set, error) { bundles := []*x509bundle.Bundle{} for _, svid := range resp.Svids { b, err := parseX509Bundle(svid.SpiffeId, svid.Bundle) if err != nil { return nil, err } bundles = append(bundles, b) } for tdID, bundle := range resp.FederatedBundles { b, err := parseX509Bundle(tdID, bundle) if err != nil { return nil, err } bundles = append(bundles, b) } return x509bundle.NewSet(bundles...), nil } func parseX509Bundle(spiffeID string, bundle []byte) (*x509bundle.Bundle, error) { td, err := spiffeid.TrustDomainFromString(spiffeID) if err != nil { return nil, err } certs, err := x509.ParseCertificates(bundle) if err != nil { return nil, err } return x509bundle.FromX509Authorities(td, certs), nil } func parseX509BundlesResponse(resp *workload.X509BundlesResponse) (*x509bundle.Set, error) { bundles := []*x509bundle.Bundle{} for tdID, b := range resp.Bundles { td, err := spiffeid.TrustDomainFromString(tdID) if err != nil { return nil, err } b, err := x509bundle.ParseRaw(td, b) if err != nil { return nil, err } bundles = append(bundles, b) } return x509bundle.NewSet(bundles...), nil } // parseJWTSVIDs parses one or all of the SVIDs in the response. If firstOnly // is true, then only the first SVID in the response is parsed and returned. // Otherwise, all SVIDs are parsed and returned. func parseJWTSVIDs(resp *workload.JWTSVIDResponse, audience []string, firstOnly bool) ([]*jwtsvid.SVID, error) { n := len(resp.Svids) if n == 0 { return nil, errors.New("there were no SVIDs in the response") } if firstOnly { n = 1 } hints := make(map[string]struct{}, n) svids := make([]*jwtsvid.SVID, 0, n) for i := 0; i < n; i++ { svid := resp.Svids[i] // In the event of more than one X509SVID message with the same hint value set, then the first message in the // list SHOULD be selected. if _, ok := hints[svid.Hint]; ok && svid.Hint != "" { continue } hints[svid.Hint] = struct{}{} s, err := jwtsvid.ParseInsecure(svid.Svid, audience) if err != nil { return nil, err } s.Hint = svid.Hint svids = append(svids, s) } return svids, nil } func parseJWTSVIDBundles(resp *workload.JWTBundlesResponse) (*jwtbundle.Set, error) { bundles := []*jwtbundle.Bundle{} for tdID, b := range resp.Bundles { td, err := spiffeid.TrustDomainFromString(tdID) if err != nil { return nil, err } b, err := jwtbundle.Parse(td, b) if err != nil { return nil, err } bundles = append(bundles, b) } return jwtbundle.NewSet(bundles...), nil } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/client_posix.go000066400000000000000000000012601474173014300252700ustar00rootroot00000000000000//go:build !windows // +build !windows package workloadapi import "errors" // appendDialOptionsOS appends OS specific dial options func (c *Client) appendDialOptionsOS() { // No options to add in this platform } func (c *Client) setAddress() error { if c.config.namedPipeName != "" { // Purely defensive. This should never happen. return errors.New("named pipes not supported in this platform") } if c.config.address == "" { var ok bool c.config.address, ok = GetDefaultAddress() if !ok { return errors.New("workload endpoint socket address is not configured") } } var err error c.config.address, err = parseTargetFromStringAddr(c.config.address) return err } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/client_test.go000066400000000000000000000476771474173014300251330ustar00rootroot00000000000000package workloadapi import ( "context" "crypto/x509" "sync" "sync/atomic" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( td = spiffeid.RequireTrustDomainFromString("example.org") federatedTD = spiffeid.RequireTrustDomainFromString("federated.test") fooID = spiffeid.RequireFromPath(td, "/foo") barID = spiffeid.RequireFromPath(td, "/bar") bazID = spiffeid.RequireFromPath(td, "/baz") hintInternal = "internal usage" hintExternal = "external usage" ) func TestFetchX509SVID(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() hint := "internal usage" resp := &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: makeX509SVIDs(ca, hint, fooID, barID), } wl.SetX509SVIDResponse(resp) svid, err := c.FetchX509SVID(context.Background()) require.NoError(t, err) assertX509SVID(t, svid, fooID, resp.SVIDs[0].Certificates, hint) } func TestFetchX509SVIDs(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() fooSVID := ca.CreateX509SVID(fooID, test.WithHint(hintInternal)) barSVID := ca.CreateX509SVID(barID, test.WithHint(hintExternal)) duplicatedHintSVID := ca.CreateX509SVID(bazID, test.WithHint(hintInternal)) emptyHintSVID1 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty1"), test.WithHint("")) emptyHintSVID2 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty2"), test.WithHint("")) resp := &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: []*x509svid.SVID{fooSVID, barSVID, duplicatedHintSVID, emptyHintSVID1, emptyHintSVID2}, } wl.SetX509SVIDResponse(resp) svids, err := c.FetchX509SVIDs(context.Background()) require.NoError(t, err) // Assert that the response contains the expected SVIDs, and does not contain the SVID with duplicated hint require.Len(t, svids, 4) assertX509SVID(t, svids[0], fooID, resp.SVIDs[0].Certificates, hintInternal) assertX509SVID(t, svids[1], barID, resp.SVIDs[1].Certificates, hintExternal) assertX509SVID(t, svids[2], emptyHintSVID1.ID, resp.SVIDs[3].Certificates, "") assertX509SVID(t, svids[3], emptyHintSVID2.ID, resp.SVIDs[4].Certificates, "") } func TestFetchX509Bundles(t *testing.T) { ca := test.NewCA(t, td) federatedCA := test.NewCA(t, federatedTD) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() wl.SetX509Bundles(ca.X509Bundle(), federatedCA.X509Bundle()) bundles, err := c.FetchX509Bundles(context.Background()) require.NoError(t, err) assert.Equal(t, 2, bundles.Len()) assertX509Bundle(t, bundles, td, ca.X509Bundle()) assertX509Bundle(t, bundles, federatedTD, federatedCA.X509Bundle()) } func TestWatchX509Bundles(t *testing.T) { wl := fakeworkloadapi.New(t) defer wl.Stop() backoffStrategy := &testBackoffStrategy{} c, err := New(context.Background(), WithAddr(wl.Addr()), WithBackoffStrategy(backoffStrategy)) require.NoError(t, err) defer c.Close() var wg sync.WaitGroup defer wg.Wait() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() tw := newTestWatcher(t) wg.Add(1) go func() { _ = c.WatchX509Bundles(ctx, tw) wg.Done() }() // test PermissionDenied tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) require.Len(t, tw.X509Bundles(), 0) // test first update ca1 := test.NewCA(t, td) wl.SetX509Bundles(ca1.X509Bundle()) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) update := tw.X509Bundles()[len(tw.X509Bundles())-1] assertX509Bundle(t, update, td, ca1.X509Bundle()) // test second update ca2 := test.NewCA(t, td) wl.SetX509Bundles(ca2.X509Bundle()) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) update = tw.X509Bundles()[len(tw.X509Bundles())-1] assertX509Bundle(t, update, td, ca2.X509Bundle()) // test error wl.Stop() tw.WaitForUpdates(1) assert.Len(t, tw.Errors(), 2) // Assert that there was the expected number of backoffs. assert.Equal(t, 2, backoffStrategy.BackedOff()) } func TestFetchX509Context(t *testing.T) { ca := test.NewCA(t, td) federatedCA := test.NewCA(t, federatedTD) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() fooSVID := ca.CreateX509SVID(fooID, test.WithHint(hintInternal)) barSVID := ca.CreateX509SVID(barID, test.WithHint(hintExternal)) duplicatedHintSVID := ca.CreateX509SVID(bazID, test.WithHint(hintInternal)) emptyHintSVID1 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty1"), test.WithHint("")) emptyHintSVID2 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty2"), test.WithHint("")) svids := []*x509svid.SVID{fooSVID, barSVID, duplicatedHintSVID, emptyHintSVID1, emptyHintSVID2} resp := &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: svids, FederatedBundles: []*x509bundle.Bundle{federatedCA.X509Bundle()}, } wl.SetX509SVIDResponse(resp) x509Ctx, err := c.FetchX509Context(context.Background()) require.NoError(t, err) // inspect svids require.Len(t, x509Ctx.SVIDs, 4) assertX509SVID(t, x509Ctx.SVIDs[0], fooID, resp.SVIDs[0].Certificates, hintInternal) assertX509SVID(t, x509Ctx.SVIDs[1], barID, resp.SVIDs[1].Certificates, hintExternal) assertX509SVID(t, x509Ctx.SVIDs[2], emptyHintSVID1.ID, resp.SVIDs[3].Certificates, "") assertX509SVID(t, x509Ctx.SVIDs[3], emptyHintSVID2.ID, resp.SVIDs[4].Certificates, "") // inspect bundles assert.Equal(t, 2, x509Ctx.Bundles.Len()) assertX509Bundle(t, x509Ctx.Bundles, td, ca.X509Bundle()) assertX509Bundle(t, x509Ctx.Bundles, federatedTD, federatedCA.X509Bundle()) // Now set the next response with an empty federated bundle and assert that the call // still succeeds. wl.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: svids, FederatedBundles: []*x509bundle.Bundle{x509bundle.FromX509Authorities(federatedCA.Bundle().TrustDomain(), nil)}, }) x509Ctx, err = c.FetchX509Context(context.Background()) require.NoError(t, err) // inspect svids require.Len(t, x509Ctx.SVIDs, 4) assertX509SVID(t, x509Ctx.SVIDs[0], fooID, resp.SVIDs[0].Certificates, hintInternal) assertX509SVID(t, x509Ctx.SVIDs[1], barID, resp.SVIDs[1].Certificates, hintExternal) assertX509SVID(t, x509Ctx.SVIDs[2], emptyHintSVID1.ID, resp.SVIDs[3].Certificates, "") assertX509SVID(t, x509Ctx.SVIDs[3], emptyHintSVID2.ID, resp.SVIDs[4].Certificates, "") } func TestWatchX509Context(t *testing.T) { ca := test.NewCA(t, td) federatedCA := test.NewCA(t, federatedTD) wl := fakeworkloadapi.New(t) defer wl.Stop() backoffStrategy := &testBackoffStrategy{} c, err := New(context.Background(), WithAddr(wl.Addr()), WithBackoffStrategy(backoffStrategy)) require.NoError(t, err) defer c.Close() ctx, cancel := context.WithCancel(context.Background()) tw := newTestWatcher(t) var wg sync.WaitGroup wg.Add(1) go func() { _ = c.WatchX509Context(ctx, tw) wg.Done() }() // test PermissionDenied tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) require.Len(t, tw.X509Contexts(), 0) fooSVID := ca.CreateX509SVID(fooID, test.WithHint(hintInternal)) barSVID := ca.CreateX509SVID(barID, test.WithHint(hintExternal)) duplicatedHintSVID := ca.CreateX509SVID(bazID, test.WithHint(hintInternal)) emptyHintSVID1 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty1"), test.WithHint("")) emptyHintSVID2 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/empty2"), test.WithHint("")) svids := []*x509svid.SVID{fooSVID, barSVID, duplicatedHintSVID, emptyHintSVID1, emptyHintSVID2} // test first update resp := &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: svids, FederatedBundles: []*x509bundle.Bundle{federatedCA.X509Bundle()}, } wl.SetX509SVIDResponse(resp) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) require.Len(t, tw.X509Contexts(), 1) update := tw.X509Contexts()[len(tw.X509Contexts())-1] // inspect svids require.Len(t, update.SVIDs, 4) assertX509SVID(t, update.SVIDs[0], fooID, resp.SVIDs[0].Certificates, hintInternal) assertX509SVID(t, update.SVIDs[1], barID, resp.SVIDs[1].Certificates, hintExternal) assertX509SVID(t, update.SVIDs[2], emptyHintSVID1.ID, resp.SVIDs[3].Certificates, "") assertX509SVID(t, update.SVIDs[3], emptyHintSVID2.ID, resp.SVIDs[4].Certificates, "") // inspect bundles assert.Equal(t, 2, update.Bundles.Len()) assertX509Bundle(t, update.Bundles, td, ca.X509Bundle()) assertX509Bundle(t, update.Bundles, federatedTD, federatedCA.X509Bundle()) bazSVID := ca.CreateX509SVID(bazID, test.WithHint(hintExternal)) // test second update resp = &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: []*x509svid.SVID{bazSVID}, } wl.SetX509SVIDResponse(resp) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) require.Len(t, tw.X509Contexts(), 2) update = tw.X509Contexts()[len(tw.X509Contexts())-1] // inspect svids require.Len(t, update.SVIDs, 1) assertX509SVID(t, update.SVIDs[0], bazID, resp.SVIDs[0].Certificates, hintExternal) // inspect bundles assert.Equal(t, 1, update.Bundles.Len()) assertX509Bundle(t, update.Bundles, td, ca.X509Bundle()) // test error wl.Stop() tw.WaitForUpdates(1) assert.Len(t, tw.Errors(), 2) cancel() wg.Wait() // Assert that there was the expected number of backoffs. assert.Equal(t, 2, backoffStrategy.BackedOff()) } func TestFetchJWTSVID(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, _ := New(context.Background(), WithAddr(wl.Addr())) defer c.Close() subjectID := spiffeid.RequireFromPath(td, "/subject") audienceID := spiffeid.RequireFromPath(td, "/audience") extraAudienceID := spiffeid.RequireFromPath(td, "/extra_audience") svid := ca.CreateJWTSVID(subjectID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("internal usage")) respJWT := makeJWTSVIDResponse(svid) wl.SetJWTSVIDResponse(respJWT) params := jwtsvid.Params{ Subject: subjectID, Audience: audienceID.String(), ExtraAudiences: []string{extraAudienceID.String()}, } jwtSvid, err := c.FetchJWTSVID(context.Background(), params) require.NoError(t, err) assertJWTSVID(t, jwtSvid, subjectID, svid.Marshal(), svid.Hint, audienceID.String(), extraAudienceID.String()) } func TestFetchJWTSVIDs(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, _ := New(context.Background(), WithAddr(wl.Addr())) defer c.Close() subjectID := spiffeid.RequireFromPath(td, "/subject") extraSubjectID := spiffeid.RequireFromPath(td, "/extra_subject") duplicatedHintID := spiffeid.RequireFromPath(td, "/somePath") audienceID := spiffeid.RequireFromPath(td, "/audience") extraAudienceID := spiffeid.RequireFromPath(td, "/extra_audience") subjectSVID := ca.CreateJWTSVID(subjectID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("internal usage")) extraSubjectSVID := ca.CreateJWTSVID(extraSubjectID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("external usage")) duplicatedHintSVID := ca.CreateJWTSVID(duplicatedHintID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("internal usage")) emptyHintSVID1 := ca.CreateJWTSVID(extraSubjectID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("")) emptyHintSVID2 := ca.CreateJWTSVID(duplicatedHintID, []string{audienceID.String(), extraAudienceID.String()}, test.WithHint("")) respJWT := makeJWTSVIDResponse(subjectSVID, extraSubjectSVID, duplicatedHintSVID, emptyHintSVID1, emptyHintSVID2) wl.SetJWTSVIDResponse(respJWT) params := jwtsvid.Params{ Subject: subjectID, Audience: audienceID.String(), ExtraAudiences: []string{extraAudienceID.String()}, } jwtSvid, err := c.FetchJWTSVIDs(context.Background(), params) require.NoError(t, err) // Assert that the response contains the expected SVIDs, and does not contain the SVID with duplicated hint require.Len(t, jwtSvid, 4) assertJWTSVID(t, jwtSvid[0], subjectID, subjectSVID.Marshal(), subjectSVID.Hint, audienceID.String(), extraAudienceID.String()) assertJWTSVID(t, jwtSvid[1], extraSubjectID, extraSubjectSVID.Marshal(), extraSubjectSVID.Hint, audienceID.String(), extraAudienceID.String()) assertJWTSVID(t, jwtSvid[2], emptyHintSVID1.ID, emptyHintSVID1.Marshal(), emptyHintSVID1.Hint, audienceID.String(), extraAudienceID.String()) assertJWTSVID(t, jwtSvid[3], emptyHintSVID2.ID, emptyHintSVID2.Marshal(), emptyHintSVID2.Hint, audienceID.String(), extraAudienceID.String()) } func TestFetchJWTBundles(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() wl.SetJWTBundles(ca.JWTBundle()) bundleSet, err := c.FetchJWTBundles(context.Background()) require.NoError(t, err) assertJWTBundle(t, bundleSet, td, ca.JWTBundle()) } func TestWatchJWTBundles(t *testing.T) { wl := fakeworkloadapi.New(t) defer wl.Stop() backoffStrategy := &testBackoffStrategy{} c, err := New(context.Background(), WithAddr(wl.Addr()), WithBackoffStrategy(backoffStrategy)) require.NoError(t, err) defer c.Close() var wg sync.WaitGroup defer wg.Wait() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() tw := newTestWatcher(t) wg.Add(1) go func() { _ = c.WatchJWTBundles(ctx, tw) wg.Done() }() // test PermissionDenied tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) require.Len(t, tw.JwtBundles(), 0) // test first update ca1 := test.NewCA(t, td) wl.SetJWTBundles(ca1.JWTBundle()) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) update := tw.JwtBundles()[len(tw.JwtBundles())-1] assertJWTBundle(t, update, td, ca1.JWTBundle()) // test second update ca2 := test.NewCA(t, td) wl.SetJWTBundles(ca2.JWTBundle()) tw.WaitForUpdates(1) require.Len(t, tw.Errors(), 1) update = tw.JwtBundles()[len(tw.JwtBundles())-1] assertJWTBundle(t, update, td, ca2.JWTBundle()) // test error wl.Stop() tw.WaitForUpdates(1) assert.Len(t, tw.Errors(), 2) // Assert that there was the expected number of backoffs. assert.Equal(t, 2, backoffStrategy.BackedOff()) } func TestValidateJWTSVID(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.New(t) defer wl.Stop() c, err := New(context.Background(), WithAddr(wl.Addr())) require.NoError(t, err) defer c.Close() workloadID := spiffeid.RequireFromPath(td, "/workload") audience := []string{"spiffe://example.org/me", "spiffe://example.org/me_too"} token := ca.CreateJWTSVID(workloadID, audience) t.Run("first audience is valid", func(t *testing.T) { jwtSvid, err := c.ValidateJWTSVID(context.Background(), token.Marshal(), audience[0]) assert.NoError(t, err) assertJWTSVID(t, jwtSvid, workloadID, token.Marshal(), "", audience...) }) t.Run("second audience is valid", func(t *testing.T) { jwtSvid, err := c.ValidateJWTSVID(context.Background(), token.Marshal(), audience[1]) assert.NoError(t, err) assertJWTSVID(t, jwtSvid, workloadID, token.Marshal(), "", audience...) }) t.Run("invalid audience returns error", func(t *testing.T) { jwtSvid, err := c.ValidateJWTSVID(context.Background(), token.Marshal(), "spiffe://example.org/not_me") assert.NotNil(t, err) assert.Nil(t, jwtSvid) }) } func makeX509SVIDs(ca *test.CA, hint string, ids ...spiffeid.ID) []*x509svid.SVID { svids := []*x509svid.SVID{} for _, id := range ids { svids = append(svids, ca.CreateX509SVID(id, test.WithHint(hint))) } return svids } func makeJWTSVIDResponse(svids ...*jwtsvid.SVID) *workload.JWTSVIDResponse { respSVIDS := []*workload.JWTSVID{} for _, svid := range svids { respSVID := &workload.JWTSVID{ SpiffeId: svid.ID.String(), Svid: svid.Marshal(), Hint: svid.Hint, } respSVIDS = append(respSVIDS, respSVID) } return &workload.JWTSVIDResponse{ Svids: respSVIDS, } } func assertX509SVID(tb testing.TB, svid *x509svid.SVID, spiffeID spiffeid.ID, certificates []*x509.Certificate, hint string) { assert.Equal(tb, spiffeID, svid.ID) assert.Equal(tb, certificates, svid.Certificates) assert.Equal(tb, hint, svid.Hint) assert.NotEmpty(tb, svid.PrivateKey) } func assertX509Bundle(tb testing.TB, bundleSet *x509bundle.Set, trustDomain spiffeid.TrustDomain, expectedBundle *x509bundle.Bundle) { b, ok := bundleSet.Get(trustDomain) require.True(tb, ok) assert.Equal(tb, b, expectedBundle) } func assertJWTBundle(tb testing.TB, bundleSet *jwtbundle.Set, trustDomain spiffeid.TrustDomain, expectedBundle *jwtbundle.Bundle) { b, ok := bundleSet.Get(trustDomain) require.True(tb, ok) assert.Equal(tb, b, expectedBundle) } func assertJWTSVID(t testing.TB, jwtSvid *jwtsvid.SVID, subjectID spiffeid.ID, token, hint string, audience ...string) { assert.Equal(t, subjectID.String(), jwtSvid.ID.String()) assert.Equal(t, audience, jwtSvid.Audience) assert.NotNil(t, jwtSvid.Claims) assert.NotEmpty(t, jwtSvid.Expiry) assert.Equal(t, token, jwtSvid.Marshal()) assert.Equal(t, hint, jwtSvid.Hint) } type testWatcher struct { t *testing.T mu sync.Mutex x509Contexts []*X509Context jwtBundles []*jwtbundle.Set x509Bundles []*x509bundle.Set errors []error updateSignal chan struct{} } func newTestWatcher(t *testing.T) *testWatcher { return &testWatcher{ t: t, updateSignal: make(chan struct{}, 100), } } func (w *testWatcher) X509Contexts() []*X509Context { w.mu.Lock() defer w.mu.Unlock() return w.x509Contexts } func (w *testWatcher) JwtBundles() []*jwtbundle.Set { w.mu.Lock() defer w.mu.Unlock() return w.jwtBundles } func (w *testWatcher) X509Bundles() []*x509bundle.Set { w.mu.Lock() defer w.mu.Unlock() return w.x509Bundles } func (w *testWatcher) Errors() []error { w.mu.Lock() defer w.mu.Unlock() return w.errors } func (w *testWatcher) OnX509ContextUpdate(u *X509Context) { w.mu.Lock() w.x509Contexts = append(w.x509Contexts, u) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) OnX509ContextWatchError(err error) { w.mu.Lock() w.errors = append(w.errors, err) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) OnJWTBundlesUpdate(u *jwtbundle.Set) { w.mu.Lock() w.jwtBundles = append(w.jwtBundles, u) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) OnX509BundlesUpdate(u *x509bundle.Set) { w.mu.Lock() w.x509Bundles = append(w.x509Bundles, u) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) OnJWTBundlesWatchError(err error) { w.mu.Lock() w.errors = append(w.errors, err) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) OnX509BundlesWatchError(err error) { w.mu.Lock() w.errors = append(w.errors, err) w.mu.Unlock() w.updateSignal <- struct{}{} } func (w *testWatcher) WaitForUpdates(expectedNumUpdates int) { numUpdates := 0 timeoutSignal := time.After(10 * time.Second) for { select { case <-w.updateSignal: numUpdates++ case <-timeoutSignal: require.Fail(w.t, "Timeout exceeding waiting for updates.") } if numUpdates == expectedNumUpdates { return } } } type testBackoffStrategy struct { backedOff int32 } func (s *testBackoffStrategy) NewBackoff() Backoff { return testBackoff{backedOff: &s.backedOff} } func (s *testBackoffStrategy) BackedOff() int { return int(atomic.LoadInt32(&s.backedOff)) } type testBackoff struct { backedOff *int32 } func (b testBackoff) Next() time.Duration { atomic.AddInt32(b.backedOff, 1) return time.Millisecond * 200 } func (b testBackoff) Reset() {} golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/client_windows.go000066400000000000000000000030431474173014300256210ustar00rootroot00000000000000//go:build windows // +build windows package workloadapi import ( "errors" "path/filepath" "strings" "github.com/Microsoft/go-winio" "google.golang.org/grpc" ) // appendDialOptionsOS appends OS specific dial options func (c *Client) appendDialOptionsOS() { if c.config.namedPipeName != "" { // Use the dialer to connect to named pipes only if a named pipe // is defined (i.e. WithNamedPipeName is used). c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) } } func (c *Client) setAddress() error { var err error if c.config.namedPipeName != "" { if c.config.address != "" { return errors.New("only one of WithAddr or WithNamedPipeName options can be used, not both") } c.config.address = namedPipeTarget(c.config.namedPipeName) return nil } if c.config.address == "" { var ok bool c.config.address, ok = GetDefaultAddress() if !ok { return errors.New("workload endpoint socket address is not configured") } } if strings.HasPrefix(c.config.address, "npipe:") { // Use the dialer to connect to named pipes only if the gRPC target // string has the "npipe" scheme c.config.dialOptions = append(c.config.dialOptions, grpc.WithContextDialer(winio.DialPipeContext)) } c.config.address, err = parseTargetFromStringAddr(c.config.address) return err } // namedPipeTarget returns a target string suitable for // dialing the endpoint address based on the provided // pipe name. func namedPipeTarget(pipeName string) string { return `\\.\` + filepath.Join("pipe", pipeName) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/client_windows_test.go000066400000000000000000000025211474173014300266600ustar00rootroot00000000000000//go:build windows // +build windows package workloadapi import ( "context" "strings" "testing" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/stretchr/testify/require" ) func TestWithNamedPipeName(t *testing.T) { ca := test.NewCA(t, td) wl := fakeworkloadapi.NewWithNamedPipeListener(t) defer wl.Stop() pipeName := strings.TrimPrefix(wl.Addr(), "npipe:") c, err := New(context.Background(), WithNamedPipeName(pipeName)) require.NoError(t, err) defer c.Close() require.Equal(t, pipeName, c.config.namedPipeName) resp := &fakeworkloadapi.X509SVIDResponse{ Bundle: ca.X509Bundle(), SVIDs: makeX509SVIDs(ca, "internal", fooID, barID), } wl.SetX509SVIDResponse(resp) svid, err := c.FetchX509SVID(context.Background()) require.NoError(t, err) assertX509SVID(t, svid, fooID, resp.SVIDs[0].Certificates, "internal") } func TestWithNamedPipeNameError(t *testing.T) { wl := fakeworkloadapi.NewWithNamedPipeListener(t) defer wl.Stop() c, err := New(context.Background(), WithNamedPipeName("ohno")) require.NoError(t, err) defer c.Close() wl.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{}) _, err = c.FetchX509SVID(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), `ohno: The system cannot find the file specified`) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/common_test.go000066400000000000000000000043561474173014300251300ustar00rootroot00000000000000package workloadapi_test import ( "testing" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/spiffebundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/stretchr/testify/require" ) func withAddr(api *fakeworkloadapi.WorkloadAPI) workloadapi.SourceOption { return workloadapi.WithClientOptions(workloadapi.WithAddr(api.Addr())) } func requireBundle(tb testing.TB, source spiffebundle.Source, td spiffeid.TrustDomain, expected *spiffebundle.Bundle) { actual, err := source.GetBundleForTrustDomain(td) require.NoError(tb, err) require.Equal(tb, expected, actual) } func requireNoBundle(tb testing.TB, source spiffebundle.Source, td spiffeid.TrustDomain, expected string) { bundle, err := source.GetBundleForTrustDomain(td) require.EqualError(tb, err, expected, "SPIFFE bundle should not exist") require.Nil(tb, bundle) } func requireX509Bundle(tb testing.TB, source x509bundle.Source, td spiffeid.TrustDomain, expected *x509bundle.Bundle) { actual, err := source.GetX509BundleForTrustDomain(td) require.NoError(tb, err) require.Equal(tb, expected, actual) } func requireNoX509Bundle(tb testing.TB, source x509bundle.Source, td spiffeid.TrustDomain, expected string) { bundle, err := source.GetX509BundleForTrustDomain(td) require.EqualError(tb, err, expected, "X.509 bundle should not exist") require.Nil(tb, bundle) } func requireJWTBundle(tb testing.TB, source jwtbundle.Source, td spiffeid.TrustDomain, expected *jwtbundle.Bundle) { actual, err := source.GetJWTBundleForTrustDomain(td) require.NoError(tb, err) require.Equal(tb, expected, actual) } func requireNoJWTBundle(tb testing.TB, source jwtbundle.Source, td spiffeid.TrustDomain, expected string) { bundle, err := source.GetJWTBundleForTrustDomain(td) require.EqualError(tb, err, expected, "JWT bundle should not exist") require.Nil(tb, bundle) } func requireX509SVID(tb testing.TB, source x509svid.Source, expected *x509svid.SVID) { actual, err := source.GetX509SVID() require.NoError(tb, err) require.Equal(tb, expected, actual) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/convenience.go000066400000000000000000000066751474173014300251030ustar00rootroot00000000000000package workloadapi import ( "context" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" ) // FetchX509SVID fetches the default X509-SVID, i.e. the first in the list // returned by the Workload API. func FetchX509SVID(ctx context.Context, options ...ClientOption) (*x509svid.SVID, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchX509SVID(ctx) } // FetchX509SVIDs fetches all X509-SVIDs. func FetchX509SVIDs(ctx context.Context, options ...ClientOption) ([]*x509svid.SVID, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchX509SVIDs(ctx) } // FetchX509Bundle fetches the X.509 bundles. func FetchX509Bundles(ctx context.Context, options ...ClientOption) (*x509bundle.Set, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchX509Bundles(ctx) } // FetchX509Context fetches the X.509 context, which contains both X509-SVIDs // and X.509 bundles. func FetchX509Context(ctx context.Context, options ...ClientOption) (*X509Context, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchX509Context(ctx) } // WatchX509Context watches for updates to the X.509 context. func WatchX509Context(ctx context.Context, watcher X509ContextWatcher, options ...ClientOption) error { c, err := New(ctx, options...) if err != nil { return err } defer c.Close() return c.WatchX509Context(ctx, watcher) } // FetchJWTSVID fetches a JWT-SVID. func FetchJWTSVID(ctx context.Context, params jwtsvid.Params, options ...ClientOption) (*jwtsvid.SVID, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchJWTSVID(ctx, params) } // FetchJWTSVID fetches all JWT-SVIDs. func FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params, options ...ClientOption) ([]*jwtsvid.SVID, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchJWTSVIDs(ctx, params) } // FetchJWTBundles fetches the JWT bundles for JWT-SVID validation, keyed // by a SPIFFE ID of the trust domain to which they belong. func FetchJWTBundles(ctx context.Context, options ...ClientOption) (*jwtbundle.Set, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.FetchJWTBundles(ctx) } // WatchJWTBundles watches for changes to the JWT bundles. func WatchJWTBundles(ctx context.Context, watcher JWTBundleWatcher, options ...ClientOption) error { c, err := New(ctx, options...) if err != nil { return err } defer c.Close() return c.WatchJWTBundles(ctx, watcher) } // WatchX509Bundles watches for changes to the X.509 bundles. func WatchX509Bundles(ctx context.Context, watcher X509BundleWatcher, options ...ClientOption) error { c, err := New(ctx, options...) if err != nil { return err } defer c.Close() return c.WatchX509Bundles(ctx, watcher) } // ValidateJWTSVID validates the JWT-SVID token. The parsed and validated // JWT-SVID is returned. func ValidateJWTSVID(ctx context.Context, token, audience string, options ...ClientOption) (*jwtsvid.SVID, error) { c, err := New(ctx, options...) if err != nil { return nil, err } defer c.Close() return c.ValidateJWTSVID(ctx, token, audience) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/examples_test.go000066400000000000000000000020221474173014300254420ustar00rootroot00000000000000package workloadapi_test import ( "context" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) func ExampleFetchX509SVID() { svid, err := workloadapi.FetchX509SVID(context.TODO()) if err != nil { // TODO: error handling } // TODO: use the X509-SVID svid = svid } func ExampleFetchJWTSVID() { serverID, err := spiffeid.FromString("spiffe://example.org/server") if err != nil { // TODO: error handling } svid, err := workloadapi.FetchJWTSVID(context.TODO(), jwtsvid.Params{ Audience: serverID.String(), }) if err != nil { // TODO: error handling } // TODO: use the JWT-SVID svid = svid } func ExampleValidateJWTSVID() { serverID, err := spiffeid.FromString("spiffe://example.org/server") if err != nil { // TODO: error handling } token := "TODO" svid, err := workloadapi.ValidateJWTSVID(context.TODO(), token, serverID.String()) if err != nil { // TODO: error handling } // TODO: use the JWT-SVID svid = svid } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/jwtsource.go000066400000000000000000000066241474173014300246260ustar00rootroot00000000000000package workloadapi import ( "context" "sync" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/zeebo/errs" ) var jwtsourceErr = errs.Class("jwtsource") // JWTSource is a source of JWT-SVID and JWT bundles maintained via the // Workload API. type JWTSource struct { watcher *watcher picker func([]*jwtsvid.SVID) *jwtsvid.SVID mtx sync.RWMutex bundles *jwtbundle.Set closeMtx sync.RWMutex closed bool } // NewJWTSource creates a new JWTSource. It blocks until the initial update // has been received from the Workload API. The source should be closed when // no longer in use to free underlying resources. func NewJWTSource(ctx context.Context, options ...JWTSourceOption) (_ *JWTSource, err error) { config := &jwtSourceConfig{} for _, option := range options { option.configureJWTSource(config) } s := &JWTSource{ picker: config.picker, } s.watcher, err = newWatcher(ctx, config.watcher, nil, s.setJWTBundles) if err != nil { return nil, err } return s, nil } // Close closes the source, dropping the connection to the Workload API. // Other source methods will return an error after Close has been called. // The underlying Workload API client will also be closed if it is owned by // the JWTSource (i.e. not provided via the WithClient option). func (s *JWTSource) Close() error { s.closeMtx.Lock() s.closed = true s.closeMtx.Unlock() return s.watcher.Close() } // FetchJWTSVID fetches a JWT-SVID from the source with the given parameters. // It implements the jwtsvid.Source interface. func (s *JWTSource) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) { if err := s.checkClosed(); err != nil { return nil, err } var ( svid *jwtsvid.SVID err error ) if s.picker == nil { svid, err = s.watcher.client.FetchJWTSVID(ctx, params) } else { svids, err := s.watcher.client.FetchJWTSVIDs(ctx, params) if err != nil { return svid, err } svid = s.picker(svids) } return svid, err } // FetchJWTSVIDs fetches all JWT-SVIDs from the source with the given parameters. // It implements the jwtsvid.Source interface. func (s *JWTSource) FetchJWTSVIDs(ctx context.Context, params jwtsvid.Params) ([]*jwtsvid.SVID, error) { if err := s.checkClosed(); err != nil { return nil, err } return s.watcher.client.FetchJWTSVIDs(ctx, params) } // GetJWTBundleForTrustDomain returns the JWT bundle for the given trust // domain. It implements the jwtbundle.Source interface. func (s *JWTSource) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) { if err := s.checkClosed(); err != nil { return nil, err } return s.bundles.GetJWTBundleForTrustDomain(trustDomain) } // WaitUntilUpdated waits until the source is updated or the context is done, // in which case ctx.Err() is returned. func (s *JWTSource) WaitUntilUpdated(ctx context.Context) error { return s.watcher.WaitUntilUpdated(ctx) } // Updated returns a channel that is sent on whenever the source is updated. func (s *JWTSource) Updated() <-chan struct{} { return s.watcher.Updated() } func (s *JWTSource) setJWTBundles(bundles *jwtbundle.Set) { s.mtx.Lock() defer s.mtx.Unlock() s.bundles = bundles } func (s *JWTSource) checkClosed() error { s.closeMtx.RLock() defer s.closeMtx.RUnlock() if s.closed { return jwtsourceErr.New("source is closed") } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/jwtsource_test.go000066400000000000000000000064141474173014300256620ustar00rootroot00000000000000package workloadapi_test import ( "context" "testing" "time" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestJWTSourceDoesNotReturnUntilInitialUpdate(t *testing.T) { api := fakeworkloadapi.New(t) defer api.Stop() // Using a timeout here to detect that it doesn't return isn't ideal. Not // sure how to deterministically test it otherwise. ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() // Create the source. It will wait for the initial response. source, err := workloadapi.NewJWTSource(ctx, withAddr(api)) if !assert.EqualError(t, err, context.DeadlineExceeded.Error()) { source.Close() } } func TestJWTSourceFailsCallsIfClosed(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) // Set the initial response api.SetJWTBundles(ca.JWTBundle()) // Create the source. It will wait for the initial response. source, err := workloadapi.NewJWTSource(ctx, withAddr(api)) require.NoError(t, err) // Close the source require.NoError(t, source.Close()) _, err = source.FetchJWTSVID(ctx, jwtsvid.Params{}) require.EqualError(t, err, "jwtsource: source is closed") _, err = source.GetJWTBundleForTrustDomain(td) require.EqualError(t, err, "jwtsource: source is closed") } func TestJWTSourceGetsUpdates(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() // Set up the CA for a few trust domains domain1TD := spiffeid.RequireTrustDomainFromString("domain1.test") domain1CA := test.NewCA(t, domain1TD) domain1Bundle := domain1CA.JWTBundle() domain2TD := spiffeid.RequireTrustDomainFromString("domain2.test") domain2CA := test.NewCA(t, domain2TD) domain2Bundle := domain2CA.JWTBundle() // Set the initial response api.SetJWTBundles(domain1CA.JWTBundle()) // Create the source. It will wait for the initial response. source, err := workloadapi.NewJWTSource(ctx, withAddr(api)) require.NoError(t, err) defer func() { assert.NoError(t, source.Close()) }() // Assert that the bundle for domain1.test is available but not domain2.test. requireJWTBundle(t, source, domain1TD, domain1Bundle) requireNoJWTBundle(t, source, domain2TD, `jwtbundle: no JWT bundle for trust domain "domain2.test"`) // Set a new response api.SetJWTBundles(domain2CA.JWTBundle()) // Wait for the source to be updated with the new response. require.NoError(t, source.WaitUntilUpdated(ctx)) // Assert that the bundle for domain1.test is no longer available and that // the bundle domain2.test is now available. requireNoJWTBundle(t, source, domain1TD, `jwtbundle: no JWT bundle for trust domain "domain1.test"`) requireJWTBundle(t, source, domain2TD, domain2Bundle) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/option.go000066400000000000000000000116011474173014300241000ustar00rootroot00000000000000package workloadapi import ( "github.com/spiffe/go-spiffe/v2/logger" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "google.golang.org/grpc" ) // ClientOption is an option used when creating a new Client. type ClientOption interface { configureClient(*clientConfig) } // WithAddr provides an address for the Workload API. The value of the // SPIFFE_ENDPOINT_SOCKET environment variable will be used if the option // is unused. func WithAddr(addr string) ClientOption { return clientOption(func(c *clientConfig) { c.address = addr }) } // WithDialOptions provides extra GRPC dialing options when dialing the // Workload API. func WithDialOptions(options ...grpc.DialOption) ClientOption { return clientOption(func(c *clientConfig) { c.dialOptions = append(c.dialOptions, options...) }) } // WithLogger provides a logger to the Client. func WithLogger(logger logger.Logger) ClientOption { return clientOption(func(c *clientConfig) { c.log = logger }) } // WithBackoff provides a custom backoff strategy that replaces the // default backoff strategy (linear backoff). func WithBackoffStrategy(backoffStrategy BackoffStrategy) ClientOption { return clientOption(func(c *clientConfig) { c.backoffStrategy = backoffStrategy }) } // SourceOption are options that are shared among all option types. type SourceOption interface { configureX509Source(*x509SourceConfig) configureJWTSource(*jwtSourceConfig) configureBundleSource(*bundleSourceConfig) } // WithClient provides a Client for the source to use. If unset, a new Client // will be created. func WithClient(client *Client) SourceOption { return withClient{client: client} } // WithClientOptions controls the options used to create a new Client for the // source. This option will be ignored if WithClient is used. func WithClientOptions(options ...ClientOption) SourceOption { return withClientOptions{options: options} } // X509SourceOption is an option for the X509Source. A SourceOption is also an // X509SourceOption. type X509SourceOption interface { configureX509Source(*x509SourceConfig) } // WithDefaultJWTSVIDPicker provides a function that is used to determine the // default JWT-SVID when more than one is provided by the Workload API. By // default, the first JWT-SVID in the list returned by the Workload API is // used. func WithDefaultJWTSVIDPicker(picker func([]*jwtsvid.SVID) *jwtsvid.SVID) JWTSourceOption { return withDefaultJWTSVIDPicker{picker: picker} } // JWTSourceOption is an option for the JWTSource. A SourceOption is also a // JWTSourceOption. type JWTSourceOption interface { configureJWTSource(*jwtSourceConfig) } // WithDefaultX509SVIDPicker provides a function that is used to determine the // default X509-SVID when more than one is provided by the Workload API. By // default, the first X509-SVID in the list returned by the Workload API is // used. func WithDefaultX509SVIDPicker(picker func([]*x509svid.SVID) *x509svid.SVID) X509SourceOption { return withDefaultX509SVIDPicker{picker: picker} } // BundleSourceOption is an option for the BundleSource. A SourceOption is also // a BundleSourceOption. type BundleSourceOption interface { configureBundleSource(*bundleSourceConfig) } type clientConfig struct { address string namedPipeName string dialOptions []grpc.DialOption log logger.Logger backoffStrategy BackoffStrategy } type clientOption func(*clientConfig) func (fn clientOption) configureClient(config *clientConfig) { fn(config) } type x509SourceConfig struct { watcher watcherConfig picker func([]*x509svid.SVID) *x509svid.SVID } type jwtSourceConfig struct { watcher watcherConfig picker func([]*jwtsvid.SVID) *jwtsvid.SVID } type bundleSourceConfig struct { watcher watcherConfig } type withClient struct { client *Client } func (o withClient) configureX509Source(config *x509SourceConfig) { config.watcher.client = o.client } func (o withClient) configureJWTSource(config *jwtSourceConfig) { config.watcher.client = o.client } func (o withClient) configureBundleSource(config *bundleSourceConfig) { config.watcher.client = o.client } type withClientOptions struct { options []ClientOption } func (o withClientOptions) configureX509Source(config *x509SourceConfig) { config.watcher.clientOptions = o.options } func (o withClientOptions) configureJWTSource(config *jwtSourceConfig) { config.watcher.clientOptions = o.options } func (o withClientOptions) configureBundleSource(config *bundleSourceConfig) { config.watcher.clientOptions = o.options } type withDefaultX509SVIDPicker struct { picker func([]*x509svid.SVID) *x509svid.SVID } func (o withDefaultX509SVIDPicker) configureX509Source(config *x509SourceConfig) { config.picker = o.picker } type withDefaultJWTSVIDPicker struct { picker func([]*jwtsvid.SVID) *jwtsvid.SVID } func (o withDefaultJWTSVIDPicker) configureJWTSource(config *jwtSourceConfig) { config.picker = o.picker } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/option_windows.go000066400000000000000000000004561474173014300256600ustar00rootroot00000000000000//go:build windows // +build windows package workloadapi // WithNamedPipeName provides a Pipe Name for the Workload API // endpoint in the form \\.\pipe\. func WithNamedPipeName(pipeName string) ClientOption { return clientOption(func(c *clientConfig) { c.namedPipeName = pipeName }) } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/watcher.go000066400000000000000000000105661474173014300242360ustar00rootroot00000000000000package workloadapi import ( "context" "sync" "github.com/spiffe/go-spiffe/v2/bundle/jwtbundle" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/zeebo/errs" ) type sourceClient interface { WatchX509Context(context.Context, X509ContextWatcher) error WatchJWTBundles(context.Context, JWTBundleWatcher) error FetchJWTSVID(context.Context, jwtsvid.Params) (*jwtsvid.SVID, error) FetchJWTSVIDs(context.Context, jwtsvid.Params) ([]*jwtsvid.SVID, error) Close() error } type watcherConfig struct { client sourceClient clientOptions []ClientOption } type watcher struct { updatedCh chan struct{} client sourceClient ownsClient bool cancel func() wg sync.WaitGroup closeMtx sync.Mutex closed bool closeErr error x509ContextFn func(*X509Context) x509ContextSet chan struct{} x509ContextSetOnce sync.Once jwtBundlesFn func(*jwtbundle.Set) jwtBundlesSet chan struct{} jwtBundlesSetOnce sync.Once } func newWatcher(ctx context.Context, config watcherConfig, x509ContextFn func(*X509Context), jwtBundlesFn func(*jwtbundle.Set)) (_ *watcher, err error) { w := &watcher{ updatedCh: make(chan struct{}, 1), client: config.client, cancel: func() {}, x509ContextFn: x509ContextFn, x509ContextSet: make(chan struct{}), jwtBundlesFn: jwtBundlesFn, jwtBundlesSet: make(chan struct{}), } // If this function fails, we need to clean up the source. defer func() { if err != nil { err = errs.Combine(err, w.Close()) } }() // Initialize a new client unless one is provided by the options if w.client == nil { client, err := New(ctx, config.clientOptions...) if err != nil { return nil, err } w.client = client w.ownsClient = true } errCh := make(chan error, 2) waitFor := func(has <-chan struct{}) error { select { case <-has: return nil case err := <-errCh: return err case <-ctx.Done(): return ctx.Err() } } // Kick up a background goroutine that watches the Workload API for // updates. var watchCtx context.Context watchCtx, w.cancel = context.WithCancel(context.Background()) if w.x509ContextFn != nil { w.wg.Add(1) go func() { defer w.wg.Done() errCh <- w.client.WatchX509Context(watchCtx, w) }() if err := waitFor(w.x509ContextSet); err != nil { return nil, err } } if w.jwtBundlesFn != nil { w.wg.Add(1) go func() { defer w.wg.Done() errCh <- w.client.WatchJWTBundles(watchCtx, w) }() if err := waitFor(w.jwtBundlesSet); err != nil { return nil, err } } // Drain the update channel since this function blocks until an update and // don't want callers to think there was an update on the source right // after it was initialized. If we ever allow the watcher to be initialzed // without waiting, this reset should be removed. w.drainUpdated() return w, nil } // Close closes the watcher, dropping the connection to the Workload API. func (w *watcher) Close() error { w.closeMtx.Lock() defer w.closeMtx.Unlock() if !w.closed { w.cancel() w.wg.Wait() // Close() can be called by New() to close a partially initialized source. // Only close the client if it has been set and the source owns it. if w.client != nil && w.ownsClient { w.closeErr = w.client.Close() } w.closed = true } return w.closeErr } func (w *watcher) OnX509ContextUpdate(x509Context *X509Context) { w.x509ContextFn(x509Context) w.triggerUpdated() w.x509ContextSetOnce.Do(func() { close(w.x509ContextSet) }) } func (w *watcher) OnX509ContextWatchError(err error) { // The watcher doesn't do anything special with the error. If logging is // desired, it should be provided to the Workload API client. } func (w *watcher) OnJWTBundlesUpdate(jwtBundles *jwtbundle.Set) { w.jwtBundlesFn(jwtBundles) w.triggerUpdated() w.jwtBundlesSetOnce.Do(func() { close(w.jwtBundlesSet) }) } func (w *watcher) OnJWTBundlesWatchError(error) { // The watcher doesn't do anything special with the error. If logging is // desired, it should be provided to the Workload API client. } func (w *watcher) WaitUntilUpdated(ctx context.Context) error { select { case <-w.updatedCh: return nil case <-ctx.Done(): return ctx.Err() } } func (w *watcher) Updated() <-chan struct{} { return w.updatedCh } func (w *watcher) drainUpdated() { select { case <-w.updatedCh: default: } } func (w *watcher) triggerUpdated() { w.drainUpdated() w.updatedCh <- struct{}{} } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/x509context.go000066400000000000000000000012161474173014300247030ustar00rootroot00000000000000package workloadapi import ( "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/svid/x509svid" ) // X509Context conveys X.509 materials from the Workload API. type X509Context struct { // SVIDs is a list of workload X509-SVIDs. SVIDs []*x509svid.SVID // Bundles is a set of X.509 bundles. Bundles *x509bundle.Set } // Default returns the default X509-SVID (the first in the list). // // See the SPIFFE Workload API standard Section 5.3. // (https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md#53-default-identity) func (x *X509Context) DefaultSVID() *x509svid.SVID { return x.SVIDs[0] } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/x509source.go000066400000000000000000000063511474173014300245240ustar00rootroot00000000000000package workloadapi import ( "context" "sync" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/zeebo/errs" ) var x509sourceErr = errs.Class("x509source") // X509Source is a source of X509-SVIDs and X.509 bundles maintained via the // Workload API. type X509Source struct { watcher *watcher picker func([]*x509svid.SVID) *x509svid.SVID mtx sync.RWMutex svid *x509svid.SVID bundles *x509bundle.Set closeMtx sync.RWMutex closed bool } // NewX509Source creates a new X509Source. It blocks until the initial update // has been received from the Workload API. The source should be closed when // no longer in use to free underlying resources. func NewX509Source(ctx context.Context, options ...X509SourceOption) (_ *X509Source, err error) { config := &x509SourceConfig{} for _, option := range options { option.configureX509Source(config) } s := &X509Source{ picker: config.picker, } s.watcher, err = newWatcher(ctx, config.watcher, s.setX509Context, nil) if err != nil { return nil, err } return s, nil } // Close closes the source, dropping the connection to the Workload API. // Other source methods will return an error after Close has been called. // The underlying Workload API client will also be closed if it is owned by // the X509Source (i.e. not provided via the WithClient option). func (s *X509Source) Close() (err error) { s.closeMtx.Lock() s.closed = true s.closeMtx.Unlock() return s.watcher.Close() } // GetX509SVID returns an X509-SVID from the source. It implements the // x509svid.Source interface. func (s *X509Source) GetX509SVID() (*x509svid.SVID, error) { if err := s.checkClosed(); err != nil { return nil, err } s.mtx.RLock() svid := s.svid s.mtx.RUnlock() if svid == nil { // This is a defensive check and should be unreachable since the source // waits for the initial Workload API update before returning from // New(). return nil, x509sourceErr.New("missing X509-SVID") } return svid, nil } // GetX509BundleForTrustDomain returns the X.509 bundle for the given trust // domain. It implements the x509bundle.Source interface. func (s *X509Source) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { if err := s.checkClosed(); err != nil { return nil, err } return s.bundles.GetX509BundleForTrustDomain(trustDomain) } // WaitUntilUpdated waits until the source is updated or the context is done, // in which case ctx.Err() is returned. func (s *X509Source) WaitUntilUpdated(ctx context.Context) error { return s.watcher.WaitUntilUpdated(ctx) } // Updated returns a channel that is sent on whenever the source is updated. func (s *X509Source) Updated() <-chan struct{} { return s.watcher.Updated() } func (s *X509Source) setX509Context(x509Context *X509Context) { var svid *x509svid.SVID if s.picker == nil { svid = x509Context.DefaultSVID() } else { svid = s.picker(x509Context.SVIDs) } s.mtx.Lock() defer s.mtx.Unlock() s.svid = svid s.bundles = x509Context.Bundles } func (s *X509Source) checkClosed() error { s.closeMtx.RLock() defer s.closeMtx.RUnlock() if s.closed { return x509sourceErr.New("source is closed") } return nil } golang-github-spiffe-go-spiffe-2.4.0/v2/workloadapi/x509source_test.go000066400000000000000000000124211474173014300255560ustar00rootroot00000000000000package workloadapi_test import ( "context" "testing" "time" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/internal/test" "github.com/spiffe/go-spiffe/v2/internal/test/fakeworkloadapi" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestX509SourceDoesNotReturnUntilInitialUpdate(t *testing.T) { api := fakeworkloadapi.New(t) defer api.Stop() // Using a timeout here to detect that it doesn't return isn't ideal. Not // sure how to deterministically test it otherwise. ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() // Create the source. It will wait for the initial response. source, err := workloadapi.NewX509Source(ctx, withAddr(api)) if !assert.EqualError(t, err, context.DeadlineExceeded.Error()) { source.Close() } } func TestX509SourceFailsCallsIfClosed(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) // Set the initial X509SVIDResponse with the X509-SVID and key api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload"))}, Bundle: ca.X509Bundle(), }) // Create the source. It will wait for the initial response. source, err := workloadapi.NewX509Source(ctx, withAddr(api)) require.NoError(t, err) // Close the source require.NoError(t, source.Close()) _, err = source.GetX509SVID() require.EqualError(t, err, "x509source: source is closed") _, err = source.GetX509BundleForTrustDomain(td) require.EqualError(t, err, "x509source: source is closed") } func TestX509SourceGetsUpdates(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() // Set up the CA for a few trust domains domain1TD := spiffeid.RequireTrustDomainFromString("domain1.test") domain1CA := test.NewCA(t, domain1TD) domain1Bundle := domain1CA.X509Bundle() domain2TD := spiffeid.RequireTrustDomainFromString("domain2.test") domain2CA := test.NewCA(t, domain2TD) domain2Bundle := domain2CA.X509Bundle() svid1 := domain1CA.CreateX509SVID(spiffeid.RequireFromPath(domain1TD, "/initial")) // Set the initial X509SVIDResponse with the X509-SVID and key api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{svid1}, Bundle: domain1Bundle, }) // Create the source. It will wait for the initial response. source, err := workloadapi.NewX509Source(ctx, withAddr(api)) require.NoError(t, err) defer func() { assert.NoError(t, source.Close()) }() // Assert that the SVID matches. requireX509SVID(t, source, svid1) // Assert that the bundle for domain1.test is available but not domain2.test. requireX509Bundle(t, source, domain1TD, domain1Bundle) requireNoX509Bundle(t, source, domain2TD, `x509bundle: no X.509 bundle for trust domain "domain2.test"`) // Swap out a new SVID and send a federated bundle with the next response. svid2 := domain1CA.CreateX509SVID(spiffeid.RequireFromPath(domain1TD, "/update")) api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{svid2}, Bundle: domain1Bundle, FederatedBundles: []*x509bundle.Bundle{domain2Bundle}, }) // Wait for the source to be updated with the new response. require.NoError(t, source.WaitUntilUpdated(ctx)) // Assert that the SVID matches. requireX509SVID(t, source, svid2) // Assert that the bundle for both trust domains are now available. requireX509Bundle(t, source, domain1TD, domain1Bundle) requireX509Bundle(t, source, domain2TD, domain2Bundle) } func TestX509SourceX509SVIDPicker(t *testing.T) { // Time out the test after a minute if something goes wrong. ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() api := fakeworkloadapi.New(t) defer api.Stop() // Set up the CA for a few trust domains td := spiffeid.RequireTrustDomainFromString("domain.test") ca := test.NewCA(t, td) svid1 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload1")) svid2 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload2")) svid3 := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/workload3")) api.SetX509SVIDResponse(&fakeworkloadapi.X509SVIDResponse{ SVIDs: []*x509svid.SVID{ svid1, svid2, svid3, }, Bundle: ca.X509Bundle(), }) // Create the source. It will wait for the initial response. source, err := workloadapi.NewX509Source(ctx, withAddr(api), workloadapi.WithDefaultX509SVIDPicker(func(svids []*x509svid.SVID) *x509svid.SVID { for _, svid := range svids { if svid.ID == svid2.ID { return svid } } assert.Fail(t, "expected X509-SVID was not found") return nil })) require.NoError(t, err) defer func() { assert.NoError(t, source.Close()) }() // Assert that the right SVID was picked. requireX509SVID(t, source, svid2) }