pax_global_header00006660000000000000000000000064151116370070014513gustar00rootroot0000000000000052 comment=c3806f63be1ed39d0bce5055a161be9352a2b438 spike-sdk-go-0.16.4/000077500000000000000000000000001511163700700141005ustar00rootroot00000000000000spike-sdk-go-0.16.4/.github/000077500000000000000000000000001511163700700154405ustar00rootroot00000000000000spike-sdk-go-0.16.4/.github/workflows/000077500000000000000000000000001511163700700174755ustar00rootroot00000000000000spike-sdk-go-0.16.4/.github/workflows/ci.yaml000066400000000000000000000017071511163700700207610ustar00rootroot00000000000000name: CI on: pull_request: push: branches: - main jobs: go-unit-test: name: Go - Unit Tests runs-on: ubuntu-latest permissions: contents: read pull-requests: read checks: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - name: Unit Tests run: make test go-lint: name: Go - Lint runs-on: ubuntu-latest permissions: contents: read pull-requests: read checks: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - name: Lint Code run: make audit spike-sdk-go-0.16.4/.gitignore000066400000000000000000000007521511163700700160740ustar00rootroot00000000000000# \\ SPIKE: Secure your secrets with SPIFFE. # \\\\\ Copyright 2024-present SPIKE contributors. # \\\\\\\ SPDX-License-Identifier: Apache-2.0 # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work go.work.sum # env file .env # Dev Env .ideaspike-sdk-go-0.16.4/.golangci.yml000066400000000000000000000006231511163700700164650ustar00rootroot00000000000000run: timeout: 1m linters: disable-all: true enable: - asciicheck - bodyclose - dogsled - errcheck - errorlint - exhaustive - gocyclo - goimports - goprintffuncname - gosec - gosimple - govet - ineffassign - misspell - nakedret - prealloc - revive - staticcheck - stylecheck - unconvert - unused - whitespace spike-sdk-go-0.16.4/CODEOWNERS000066400000000000000000000007061511163700700154760ustar00rootroot00000000000000# \\ SPIKE: Secure your secrets with SPIFFE. # \\\\\ Copyright 2024-present SPIKE contributors. # \\\\\\\ SPDX-License-Identifier: Apache-2.0 # Lines starting with '#' are comments. # Each line is a file pattern followed by one or more owners. # More details are here: https://help.github.com/articles/about-codeowners/ ## Code Owners (in no particular order) ## * @v0lkan @kfox1111 @parlakisik # Volkan Özçelik, Kevin Fox, Murat Parlakışık spike-sdk-go-0.16.4/CODE_OF_CONDUCT.md000066400000000000000000000010041511163700700166720ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) This repository follows the [SPIFFE Code of Conduct][coc]. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. [coc]: https://github.com/spiffe/spiffe/blob/main/CODE-OF-CONDUCT.md As a quick summary, while contributing to this repository, and engaging with the SPIFFE community, **be a good citizen**, **be inclusive**, and **be respectful**. For details, please refer to the [SPIFFE Code of Conduct][coc].spike-sdk-go-0.16.4/CONTRIBUTING.md000066400000000000000000000024411511163700700163320ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) ## Welcome Thank you for your interest in contributing to **SPIKE** 🤘. We appreciate any help, be it in the form of code, documentation, design, or even bug reports and feature requests. When contributing to this repository, please first discuss the change you wish to make via an issue, email, or any other method before making a change. This way, we can avoid misunderstandings and wasted effort. One great way to initiate such discussions is asking a question [SPIFFE Slack Community][slack]. [slack]: https://slack.spiffe.io/ "Join SPIFFE on Slack" Please note that [we have a code of conduct](CODE_OF_CONDUCT.md). We expect all contributors to adhere to it in all interactions with the project. Also, make sure you read, understand and accept [The Developer Certificate of Origin Contribution Guide](CONTRIBUTING_DCO.md) as it is a requirement to contribute to this project and contains more details about the contribution process. ## How To Run Tests Before merging your changes or creating a Pull Request, make sure all tests pass and code quality checks are satisfied. To test the SDK locally and run quality checks, use the following commands in the project root: ```bash # Run all tests make test # Run code quality and security audit make audit ``` spike-sdk-go-0.16.4/CONTRIBUTING_DCO.md000066400000000000000000000102621511163700700170170ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) ## Contributing to SPIKE We welcome contributions from the community and first want to thank you for taking the time to contribute! Please familiarize yourself with our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. Before you start working with SPIKE, please read our [Developer Certificate of Origin](CONTRIBUTING_DCO.md). All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. We appreciate any help, be it in the form of code, documentation, design, or even bug reports and feature requests. When contributing to this repository, please first discuss the change you wish to make via an issue, email, or any other method before making a change. This way, we can avoid misunderstandings and wasted effort. One great way to initiate such discussions is asking a question [SPIFFE Slack Community][slack]. [slack]: https://slack.spiffe.io/ "Join SPIFFE on Slack" Please note that [we have a code of conduct](CODE_OF_CONDUCT.md). We expect all contributors to adhere to it in all interactions with the project. ## Ways to contribute We welcome many different types of contributions and not all of them need a Pull request. Contributions may include: * New features and proposals * Documentation * Bug fixes * Issue Triage * Answering questions and giving feedback * Helping to onboard new contributors * Other related activities ## Getting started Please [quickstart guide][use-the-source] to learn how to build, deploy, and test **SPIKE** from the source. [use-the-source]: https://spike.ist/#/quickstart The quickstart guide also includes common errors that you might find when building, deploying, and testing **SPIKE**. ## Contribution Flow This is a rough outline of what a contributor's workflow looks like: * Make a fork of the repository within your GitHub account. * Create a topic branch in your fork from where you want to base your work * Make commits of logical units. * Make sure your commit messages are with the proper format, quality and descriptiveness (*see below*) * Adhere to the code standards described below. * Push your changes to the topic branch in your fork * Ensure all components build and function properly. * Update necessary `README.md` and other documents to reflect your changes. * Keep pull requests as granular as possible. Reviewing large amounts of code can be error-prone and time-consuming for the reviewers. * Create a pull request containing that commit. * Engage in the discussion under the pull request and proceed accordingly. ## Pull Request Checklist Before submitting your pull request, we advise you to use the following: 1. Check if your code changes will pass local tests (*i.e., `go test ./...` should exit with a `0` success status code*). 2. Ensure your commit messages are descriptive. We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). Be sure to include any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits. 3. Check the commits and commits messages and ensure they are free from typos. ## Reporting Bugs and Creating Issues For specifics on what to include in your report, please follow the guidelines in the issue and pull request templates when available. ## Ask for Help The best way to reach us with a question when contributing is to ask on: * The original GitHub issue * [**SPIFFE Slack Workspace**][slack-invite] ### Code Standards In **SPIKE**, we aim for a unified and clean codebase. When contributing, please try to match the style of the code that you see in the file you're working on. The file should look as if it was authored by a single person after your changes. For Go files, we require that you run `gofmt` before submitting your pull request to ensure consistent formatting. ### Testing Before submitting your pull request, make sure your changes pass all the existing tests, and add new ones if necessary. [slack-invite]: https://slack.spiffe.io/ "Join SPIFFE Slack" spike-sdk-go-0.16.4/LICENSE000066400000000000000000000261351511163700700151140ustar00rootroot00000000000000 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. spike-sdk-go-0.16.4/MAINTAINERS.md000066400000000000000000000001051511163700700161700ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) See [`CODEOWNERS`](CODEOWNERS).spike-sdk-go-0.16.4/Makefile000066400000000000000000000075251511163700700155510ustar00rootroot00000000000000# \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ # \\\\\ Copyright 2024-present SPIKE contributors. # \\\\\\\ SPDX-License-Identifier: Apache-2.0 .PHONY: test/cover test audit ci confirm no-dirty upgradeable tidy coverage/publish # Run tests with coverage report and open HTML visualization # Usage: make test/cover # Executes all tests with race detection and coverage profiling # Generates an HTML coverage report and opens it in the default browser # Coverage data is temporarily stored in /tmp/coverage.out # Flags: -v (verbose), -race (race detection), -buildvcs (include VCS info) test/cover: go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... go tool cover -html=/tmp/coverage.out # Generate and publish HTML coverage report to documentation # Usage: make coverage/publish # Executes all tests with coverage profiling and generates an HTML report # The report is published to /home/volkan/WORKSPACE/spike/docs/sdk/coverage.html # This is useful for maintaining published documentation of test coverage coverage/publish: @./hack/coverage-report.sh # Run all tests with race detection # Usage: make test # Executes all tests in the project with verbose output and race detection # Does not generate coverage reports (use test/cover for that) # Flags: -v (verbose), -race (race detection), -buildvcs (include VCS info) test: go test -v -race -buildvcs ./... # Comprehensive code quality audit # Usage: make audit # Performs multiple quality checks: # 1. go mod tidy -diff: checks if go.mod needs tidying # (fails if changes needed) # 2. go mod verify: verifies module dependencies haven't been tampered with # 3. gofmt check: ensures all Go files are properly formatted # 4. go vet: runs Go's built-in static analysis # 5. staticcheck: runs advanced static analysis # (excluding ST1000, U1000 checks) # 6. govulncheck: scans for known security vulnerabilities # 7. golangci-lint: runs a comprehensive set of linters # (follows the configuration in .golangci.yml) audit: go mod tidy -diff go mod verify test -z "$(shell gofmt -l .)" go vet ./... go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./... go run golang.org/x/vuln/cmd/govulncheck@latest ./... CGO_ENABLED=0 go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run # Comprehensive set of checks to simulate a CI environment # Usage: make ci # Prerequisites: # 1. runs 'test' target first to ensure tests pass # 2. runs 'audit' target to perform code quality checks ci: test audit # Interactive confirmation prompt # Usage: make confirm && make some-destructive-action # Prompts user for confirmation before proceeding with potentially destructive # operations # Returns successfully only if user explicitly types 'y', defaults to 'N' on # empty input confirm: @echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] # Check for uncommitted changes in git repository # Usage: make no-dirty # Ensures the working directory is clean with no uncommitted changes # Useful as a prerequisite for deployment or release targets # Exits with error code if there are any modified, added, or untracked files no-dirty: @test -z "$(shell git status --porcelain)" # Check for available Go module upgrades # Usage: make upgradeable # Downloads and runs go-mod-upgrade tool to display available dependency updates # Does not actually upgrade anything, only shows what could be upgraded # Requires internet connection to fetch the tool and check for updates upgradeable: @go run github.com/oligot/go-mod-upgrade@latest # Clean up Go module dependencies and format code # Usage: make tidy # Performs two operations: # 1. go mod tidy -v: removes unused dependencies and adds missing ones # 2. go fmt ./...: formats all Go source files in the project # Should be run before committing code changes tidy: go mod tidy -v go fmt ./... spike-sdk-go-0.16.4/README.md000066400000000000000000000052101511163700700153550ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) ## SPIKE Go SDK This library is a convenient Go library for working with [SPIKE][spike-web]. It leverages the [SPIFFE Workload API][workload-api], providing high-level functionality that includes: * Establishing mutually authenticated TLS (*mTLS*) between workloads powered by [SPIFFE][spiffe-web]. * Abstracting SPIKE REST API calls. [workload-api]: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md "SPIFFE Workload API" [spike-web]: https://spike.ist/ "SPIKE" [spiffe-web]: https://spiffe.io/ "SPIFFE" ## Documentation See the [Go Package](https://pkg.go.dev/github.com/spiffe/spike-sdk-go) 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`). ## Usage Example ```go package main import ( "fmt" spike "github.com/spiffe/spike-sdk-go/impl" ) func main() { api := spike.New() // Use the default Workload API Socket defer api.Close() // Close the connection when done path := "tenants/demo/db/creds" // Create a Secret err := api.PutSecret(path, map[string]string{ "username": "SPIKE", "password": "SPIKE_Rocks", }) if err != nil { fmt.Println("Error writing secret:", err.Error()) return } // Read the Secret secret, err := api.GetSecret(path) if err != nil { fmt.Println("Error reading secret:", err.Error()) return } if secret == nil { fmt.Println("Secret not found.") return } fmt.Println("Secret found:") data := secret.Data for k, v := range data { fmt.Printf("%s: %s\n", k, v) } } ``` ## A Note on Security We take **SPIKE**'s security seriously. If you believe you have found a vulnerability, please responsibily disclose it to [security@spike.ist](mailto:security@spike.ist). See [SECURITY.md](SECURITY.md) for additional details. ## Community Open Source is better together. If you are a security enthusiast, [join SPIFFE's Slack Workspace][spiffe-slack] and let us change the world together 🤘. # Contributing To contribute to **SPIKE**, [follow the contributing guidelines](CONTRIBUTING.md) to get started. Use GitHub issues to request features or file bugs. ## Communications * [SPIFFE **Slack** is where the community hangs out][spiffe-slack]. * [Send comments and suggestions to **feedback@spike.ist**](mailto:feedback@spike.ist). ## License [Mozilla Public License v2.0](LICENSE). [spiffe-slack]: https://slack.spiffe.io/ [spiffe]: https://spiffe.io/ [spike]: https://spike.ist/ [quickstart]: https://spike.ist/#/quickstart spike-sdk-go-0.16.4/SECURITY.md000066400000000000000000000047121511163700700156750ustar00rootroot00000000000000![SPIKE](assets/spike-banner-lg.png) ## About This document outlines the security policy and procedures for reporting security vulnerabilities in **SPIKE**, along with the version support policy. ## Supported Versions Only the most recent version of **SPIKE** is currently being supported with security updates. Note that **SPIKE** consists of more than a single component, and during a release cut, all components are signed and tagged with the same version. After **SPIKE** hits a major 1.0.0. Version, this will change, and we will also have a support plan for various major versions. ## Reporting a Vulnerability We are very thankful for—and if desired, happy to credit—security researchers and users who report vulnerabilities to the SPIKE community. Please send your vulnerability reports to [security@spike.ist](mailto:spike.ist). We don't have an official turnover time, but if nobody gets back to you within a week, please send another email. We take all vulnerability reports seriously, and you will be notified if your report is accepted or declined, and what further actions we are going to take on it. ## Handling * All reports are thoroughly investigated by SPIKE Maintainers. * Any vulnerability information shared will not be shared with others unless it is necessary to fix the issue. Information is shared only on a "*need to know*" basis. * As the security issue moves through the identification and resolution process, the reporter will be notified. * Additional questions about the vulnerability may also be asked of the reporter. Note that while SPIKE is very active, it is a vendor-neutral CNCF-overseen project maintained by volunteers, not by a single Company. As such, security issue handling is done on a best-effort basis. ## Disclosures We will coordinate publishing disclosures and security releases in a way that is realistic and necessary for end users. We prefer to fully disclose the vulnerability as soon as possible once user mitigation is available. Disclosures will always be published in a timely manner after a release is published that fixes the vulnerability. A disclosure will likely (*but not always*) contain an overview, details about the vulnerability, a fix that is typically an update or a patch, and optionally a workaround if available. Links to disclosures will also be added to the [SPIKE Changelog][changelog] once we publish a fix for a vulnerability. [changelog]: https://spike.ist/tracking/changelog/ "SPIKE Changelog" spike-sdk-go-0.16.4/api/000077500000000000000000000000001511163700700146515ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/api_test.go000066400000000000000000000074531511163700700170210ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api // TODO: FIXME //// stub source (nil pointer acceptable for our stubs) //var fakeSource *workloadapi.X509Source // //func TestAPI_CipherStreamMethods(t *testing.T) { // a := NewWithSource(fakeSource) // // // backup and restore // origEnc, origDec := cipherEncrypt, cipherDecrypt // t.Cleanup(func() { cipherEncrypt, cipherDecrypt = origEnc, origDec }) // // cipherEncrypt = func(_ *workloadapi.X509Source, // mode cipher.Mode, r io.Reader, contentType string, // _ []byte, _ string, _ predicate.Predicate, // ) ([]byte, error) { // if mode != cipher.ModeStream || contentType != "application/octet-stream" { // return nil, errors.New("bad mode or content-type") // } // b, _ := io.ReadAll(r) // if string(b) != "plain" { // return nil, errors.New("unexpected input") // } // return []byte("cipher"), nil // } // cipherDecrypt = func(_ *workloadapi.X509Source, mode cipher.Mode, // r io.Reader, contentType string, _ byte, _, _ []byte, _ string, // _ predicate.Predicate) ([]byte, error) { // if mode != cipher.ModeStream || contentType != "application/octet-stream" { // return nil, errors.New("bad mode or content-type") // } // b, _ := io.ReadAll(r) // if string(b) != "cipher" { // return nil, errors.New("unexpected input") // } // return []byte("plain"), nil // } // // out, err := a.CipherEncryptStream( // bytes.NewReader([]byte("plain")), "application/octet-stream") // if err != nil { // t.Fatalf("CipherEncryptStream error: %v", err) // } // if string(out) != "cipher" { // t.Fatalf("unexpected encrypt out: %s", string(out)) // } // // out2, err := a.CipherDecryptStream( // bytes.NewReader([]byte("cipher")), "application/octet-stream") // if err != nil { // t.Fatalf("CipherDecryptStream error: %v", err) // } // if string(out2) != "plain" { // t.Fatalf("unexpected decrypt out: %s", string(out2)) // } // // // error path // cipherEncrypt = func(_ *workloadapi.X509Source, // _ cipher.Mode, _ io.Reader, _ string, _ []byte, _ string, // _ predicate.Predicate) ([]byte, error) { // return nil, errors.New("boom") // } // if _, err := a.CipherEncryptStream( // bytes.NewReader(nil), "application/octet-stream"); err == nil { // t.Fatalf("expected error from CipherEncryptStream") // } //} // //func TestAPI_CipherJSONMethods(t *testing.T) { // a := NewWithSource(fakeSource) // // origEnc, origDec := cipherEncrypt, cipherDecrypt // t.Cleanup(func() { cipherEncrypt, cipherDecrypt = origEnc, origDec }) // // cipherEncrypt = func( // _ *workloadapi.X509Source, mode cipher.Mode, _ io.Reader, // _ string, plaintext []byte, algorithm string, _ predicate.Predicate, // ) ([]byte, error) { // if mode != cipher.ModeJSON { // return nil, errors.New("bad mode") // } // if string(plaintext) != "p" || algorithm != "alg" { // return nil, errors.New("bad input") // } // return []byte{2}, nil // } // cipherDecrypt = func( // _ *workloadapi.X509Source, mode cipher.Mode, _ io.Reader, // _ string, version byte, _, _ []byte, // algorithm string, _ predicate.Predicate, // ) ([]byte, error) { // if mode != cipher.ModeJSON { // return nil, errors.New("bad mode") // } // if version != 1 || algorithm != "alg" { // return nil, errors.New("bad input") // } // return []byte("p"), nil // } // // out, err := a.CipherEncrypt([]byte("p"), "alg") // if err != nil { // t.Fatalf("CipherEncrypt error: %v", err) // } // if len(out) != 1 { // t.Fatalf("unexpected encrypt json response length: %d", len(out)) // } // // outp, err := a.CipherDecrypt(1, []byte{1}, []byte{2}, "alg") // if err != nil { // t.Fatalf("CipherDecrypt error: %v", err) // } // if string(outp) != "p" { // t.Fatalf("unexpected decrypt json: %s", string(outp)) // } //} spike-sdk-go-0.16.4/api/bootstrap.go000066400000000000000000000047141511163700700172230ustar00rootroot00000000000000package api import ( "github.com/cloudflare/circl/secretsharing" "github.com/spiffe/spike-sdk-go/api/internal/impl/bootstrap" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // Contribute sends a secret share contribution to a SPIKE Keeper during the // bootstrap process. // // It establishes a mutual TLS connection to the specified Keeper and transmits // the keeper's share of the secret. The function marshals the share value, // validates its length, and sends it securely to the Keeper. After sending, the // contribution is zeroed out in memory for security. // // Parameters: // - keeperShare: The secret share to contribute to the Keeper // - keeperID: The unique identifier of the target Keeper // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - Errors from net.Post(): if the HTTP request fails // // Note: The function will fatally crash (via log.FatalErr) if: // - Marshal failures (ErrDataMarshalFailure) // - Share length validation fails (ErrCryptoInvalidEncryptionKeyLength) func (a *API) Contribute( keeperShare secretsharing.Share, keeperID string, ) *sdkErrors.SDKError { return bootstrap.Contribute(a.source, keeperShare, keeperID) } // Verify performs bootstrap verification with SPIKE Nexus by sending encrypted // random text and validating that Nexus can decrypt it correctly. // // This ensures that the bootstrap process completed successfully and Nexus has // the correct master key. The function sends the nonce and ciphertext to Nexus, // receives back a hash, and compares it against the expected hash of the // original random text. A match confirms successful bootstrap. // // Parameters: // - randomText: The original random text that was encrypted // - nonce: The nonce used during encryption // - cipherText: The encrypted random text // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - Errors from net.Post(): if the HTTP request fails // // Note: The function will fatally crash (via log.FatalErr) if: // - Marshal failures (ErrDataMarshalFailure) // - Response parsing failures (ErrDataUnmarshalFailure) // - Hash verification fails (ErrCryptoCipherVerificationFailed) func (a *API) Verify( randomText string, nonce, cipherText []byte, ) *sdkErrors.SDKError { return bootstrap.Verify(a.source, randomText, nonce, cipherText) } spike-sdk-go-0.16.4/api/cipher.go000066400000000000000000000104071511163700700164540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( "io" "github.com/spiffe/spike-sdk-go/api/internal/impl/cipher" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // CipherEncryptStream encrypts data from a reader using streaming mode. // // It sends the reader content as the request body to SPIKE Nexus for encryption. // The data is treated as binary (application/octet-stream) regardless of its // original format, as encryption operates on raw bytes. // // Parameters: // - reader: The data source to encrypt // // Returns: // - []byte: The encrypted ciphertext if successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // reader := strings.NewReader("sensitive data") // encrypted, err := api.CipherEncryptStream(reader) func (a *API) CipherEncryptStream( reader io.Reader, ) ([]byte, *sdkErrors.SDKError) { return cipher.EncryptStream(a.source, reader) } // CipherEncrypt encrypts data with structured parameters. // // It sends plaintext and algorithm to SPIKE Nexus and returns the // encrypted ciphertext bytes. // // Parameters: // - plaintext: The data to encrypt // - algorithm: The encryption algorithm to use (e.g., "AES-GCM") // // Returns: // - []byte: The encrypted ciphertext if successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // data := []byte("secret message") // encrypted, err := api.CipherEncrypt(data, "AES-GCM") func (a *API) CipherEncrypt( plaintext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { return cipher.Encrypt(a.source, plaintext, algorithm) } // CipherDecryptStream decrypts data from a reader using streaming mode. // // It sends the reader content as the request body to SPIKE Nexus for decryption. // The data is treated as binary (application/octet-stream) as decryption // operates on raw encrypted bytes. // // Parameters: // - reader: The encrypted data source to decrypt // // Returns: // - []byte: The decrypted plaintext if successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // reader := bytes.NewReader(encryptedData) // plaintext, err := api.CipherDecryptStream(reader) func (a *API) CipherDecryptStream( reader io.Reader, ) ([]byte, *sdkErrors.SDKError) { return cipher.DecryptStream(a.source, reader) } // CipherDecrypt decrypts data with structured parameters. // // It sends version, nonce, ciphertext, and algorithm to SPIKE Nexus // and returns the decrypted plaintext. // // Parameters: // - version: The cipher version used during encryption // - nonce: The nonce bytes used during encryption // - ciphertext: The encrypted data to decrypt // - algorithm: The encryption algorithm used (e.g., "AES-GCM") // // Returns: // - []byte: The decrypted plaintext if successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // plaintext, err := api.CipherDecrypt(1, nonce, ciphertext, "AES-GCM") func (a *API) CipherDecrypt( version byte, nonce, ciphertext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { return cipher.Decrypt(a.source, version, nonce, ciphertext, algorithm) } spike-sdk-go-0.16.4/api/cleanup.go000066400000000000000000000017671511163700700166420ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/spiffe" ) // Close releases any resources held by the API instance. // // It ensures proper cleanup of the underlying X509Source. This method should // be called when the API instance is no longer needed, typically during // application shutdown or cleanup. // // Returns: // - *sdkErrors.SDKError: nil if successful or source is nil, // ErrSPIFFEFailedToCreateX509Source if closure fails // // Example: // // api, err := NewAPI(ctx) // if err != nil { // log.Fatal(err) // } // defer func() { // if err := api.Close(); err != nil { // log.Printf("Failed to close API: %v", err) // } // }() func (a *API) Close() *sdkErrors.SDKError { if a.source == nil { return nil } return spiffe.CloseSource(a.source) } spike-sdk-go-0.16.4/api/construct.go000066400000000000000000000055331511163700700172320ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( "context" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/spiffe" ) // API is the SPIKE API. type API struct { source *workloadapi.X509Source } // New creates and returns a new instance of API configured with a SPIFFE // source. // // It automatically discovers and connects to the SPIFFE Workload API endpoint // using the default socket path and creates an X.509 source for authentication // with a configurable timeout to prevent indefinite blocking on socket issues. // // The timeout can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT // environment variable (default: 30s). // // The API client is configured to communicate exclusively with SPIKE Nexus. // // Returns: // - *API: A configured API instance ready for use, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFEFailedToCreateX509Source: if X509Source creation fails // - ErrSPIFFEUnableToFetchX509Source: if initial SVID fetch fails // // Example: // // api, err := New() // if err != nil { // log.Fatalf("Failed to initialize SPIKE API: %v", err) // } // defer api.Close() func New() (*API, *sdkErrors.SDKError) { defaultEndpointSocket := spiffe.EndpointSocket() ctx, cancel := context.WithTimeout( context.Background(), env.SPIFFESourceTimeoutVal(), ) defer cancel() source, _, err := spiffe.Source(ctx, defaultEndpointSocket) if err != nil { return nil, err } return &API{source: source}, nil } // NewWithSource initializes a new API instance with a pre-configured // X509Source. This constructor is useful when you already have an X.509 source // or need custom source configuration. The API instance will be configured to // only communicate with SPIKE Nexus servers. // // Parameters: // - source: A pre-configured X509Source that provides the client's identity // certificates and trusted roots for server validation // // Returns: // - *API: A configured API instance using the provided source // // Note: The API client created with this function is restricted to communicate // only with SPIKE Nexus instances (using predicate.AllowNexus). If you need // to connect to different servers, use New() with a custom predicate instead. // // Example usage: // // // Use with custom-configured source // source, err := workloadapi.NewX509Source(ctx, // workloadapi.WithClientOptions(...)) // if err != nil { // log.Fatal("Failed to create X509Source") // } // api := NewWithSource(source) // defer api.Close() func NewWithSource(source *workloadapi.X509Source) *API { return &API{ source: source, } } spike-sdk-go-0.16.4/api/doc.go000066400000000000000000000026711511163700700157530ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package api provides the primary client interface for interacting with // SPIKE services. // // The API type serves as the main entry point for all SPIKE operations, // supporting: // - Secret management (create, read, update, delete, list, and version // control) // - Policy management (create, read, update, delete, and list access // control policies) // - Cryptographic operations (encrypt and decrypt via streaming or // JSON modes) // - Operator functions (recover and restore using Shamir secret sharing) // // All operations use mutual TLS authentication with SPIFFE X.509 certificates // and communicate exclusively with SPIKE Nexus servers by default. // // Example usage: // // // Create a new API client // api := api.New() // if api == nil { // log.Fatal("Failed to initialize SPIKE API") // } // defer api.Close() // // // Store a secret // err := api.PutSecret("app/db/password", map[string]string{ // "username": "admin", // "password": "secret123", // }) // // // Retrieve a secret // secret, err := api.GetSecret("app/db/password") // // // Create an access policy // err = api.CreatePolicy( // "db-access", // "spiffe://example.org/app/*", // "app/db/*", // []data.PolicyPermission{data.PermissionRead}, // ) package api spike-sdk-go-0.16.4/api/entity/000077500000000000000000000000001511163700700161655ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/entity/data/000077500000000000000000000000001511163700700170765ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/entity/data/doc.go000066400000000000000000000007421511163700700201750ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package data provides data structures for the SPIKE mTLS REST APIs. // It includes types for secrets, metadata, policies, permissions, and operator // status. These structures are used for serializing and deserializing API // requests and responses when communicating with the SPIKE services. package data spike-sdk-go-0.16.4/api/entity/data/metadata.go000066400000000000000000000016671511163700700212170ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package data import "time" // SecretVersionInfo for the secret's version type SecretVersionInfo struct { CreatedTime time.Time `json:"createdTime"` Version int `json:"version"` DeletedTime *time.Time `json:"deletedTime"` } // SecretMetaDataContent for the secret's raw metadata type SecretMetaDataContent struct { CurrentVersion int `json:"currentVersion"` OldestVersion int `json:"oldestVersion"` CreatedTime time.Time `json:"createdTime"` UpdatedTime time.Time `json:"updatedTime"` MaxVersions int `json:"maxVersions"` } // SecretMetadata for the secret's metadata type SecretMetadata struct { Versions map[int]SecretVersionInfo `json:"versions,omitempty"` Metadata SecretMetaDataContent `json:"metadata,omitempty"` } spike-sdk-go-0.16.4/api/entity/data/operator.go000066400000000000000000000007441511163700700212650ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package data // RestorationStatus represents the status of a restoration process, // including collected shards, remaining shards, and completion status. type RestorationStatus struct { ShardsCollected int `json:"collected"` ShardsRemaining int `json:"remaining"` Restored bool `json:"restored"` } spike-sdk-go-0.16.4/api/entity/data/policy.go000066400000000000000000000045441511163700700207330ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package data import ( "regexp" "time" ) type PolicyPermission string // PermissionRead gives permission to read secrets. // This DOES NOT include listing secrets. const PermissionRead PolicyPermission = "read" // PermissionWrite gives permission to write (including // create, update and delete) secrets. const PermissionWrite PolicyPermission = "write" // PermissionList gives permission to list available secrets or resources. const PermissionList PolicyPermission = "list" // TODO: verify proper usage of PermissionExecute in SPIKE code. // UPDATE CHANGELOG if behavior changes. // PermissionExecute grants the ability to execute specified resources. // One such resource is encryption and decryption operations that // don't necessarily persist anything but execute an internal command. const PermissionExecute PolicyPermission = "execute" // PermissionSuper gives superuser permissions. // The user is the alpha and the omega. const PermissionSuper PolicyPermission = "super" // Policy represents a security policy applied within SPIKE. // It includes details such as ID, name, patterns, permissions, and metadata. type Policy struct { ID string `json:"id"` Name string `json:"name"` SPIFFEIDPattern string `json:"spiffiedPattern"` PathPattern string `json:"pathPattern"` Permissions []PolicyPermission `json:"permissions"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` // Unexported fields won't be serialized to JSON IDRegex *regexp.Regexp `json:"-"` PathRegex *regexp.Regexp `json:"-"` } // PolicySpec defines the specification of a policy configuration. // Name specifies the name of the policy. // SpiffeIDPattern specifies the SPIFFE ID regex pattern for the policy. // PathPattern defines the path regex pattern associated with the policy. // Permissions lists the permissions granted by the policy. type PolicySpec struct { Name string `yaml:"name"` SpiffeIDPattern string `yaml:"spiffeidPattern"` PathPattern string `yaml:"pathPattern"` Permissions []PolicyPermission `json:"permissions"` } spike-sdk-go-0.16.4/api/entity/data/secret.go000066400000000000000000000004751511163700700207200ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package data // Secret is the secret that returns from SPIKE Nexus mTLS REST API. type Secret struct { Data map[string]string `json:"data"` } spike-sdk-go-0.16.4/api/entity/v1/000077500000000000000000000000001511163700700165135ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/entity/v1/reqres/000077500000000000000000000000001511163700700200145ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/entity/v1/reqres/bootstrap.go000066400000000000000000000027231511163700700223640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // BootstrapVerifyRequest for verifying SPIKE Nexus initialization. type BootstrapVerifyRequest struct { // Nonce used for encryption Nonce []byte `json:"nonce"` // Encrypted ciphertext to verify Ciphertext []byte `json:"ciphertext"` } // BootstrapVerifyResponse contains the hash of the decrypted plaintext. type BootstrapVerifyResponse struct { // Hash of the decrypted plaintext Hash string `json:"hash"` // Error code if operation failed Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r BootstrapVerifyResponse) Success() BootstrapVerifyResponse { r.Err = "" return r } func (r BootstrapVerifyResponse) NotFound() BootstrapVerifyResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r BootstrapVerifyResponse) BadRequest() BootstrapVerifyResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r BootstrapVerifyResponse) Unauthorized() BootstrapVerifyResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r BootstrapVerifyResponse) Internal() BootstrapVerifyResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r BootstrapVerifyResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/cipher.go000066400000000000000000000055161511163700700216240ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // CipherEncryptRequest for encrypting data type CipherEncryptRequest struct { // Plaintext data to encrypt Plaintext []byte `json:"plaintext"` // Optional: specify encryption algorithm/version Algorithm string `json:"algorithm,omitempty"` } // CipherEncryptResponse contains encrypted data type CipherEncryptResponse struct { // Version byte for future compatibility Version byte `json:"version"` // Nonce used for encryption Nonce []byte `json:"nonce"` // Encrypted ciphertext Ciphertext []byte `json:"ciphertext"` // Error code if operation failed Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r CipherEncryptResponse) Success() CipherEncryptResponse { r.Err = "" return r } func (r CipherEncryptResponse) NotFound() CipherEncryptResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r CipherEncryptResponse) BadRequest() CipherEncryptResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r CipherEncryptResponse) Unauthorized() CipherEncryptResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r CipherEncryptResponse) Internal() CipherEncryptResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r CipherEncryptResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // CipherDecryptRequest for decrypting data type CipherDecryptRequest struct { // Version byte to determine decryption method Version byte `json:"version"` // Nonce used during encryption Nonce []byte `json:"nonce"` // Encrypted ciphertext to decrypt Ciphertext []byte `json:"ciphertext"` // Optional: specify decryption algorithm/version Algorithm string `json:"algorithm,omitempty"` } // CipherDecryptResponse contains decrypted data type CipherDecryptResponse struct { // Decrypted plaintext data Plaintext []byte `json:"plaintext"` // Error code if operation failed Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r CipherDecryptResponse) Success() CipherDecryptResponse { r.Err = "" return r } func (r CipherDecryptResponse) NotFound() CipherDecryptResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r CipherDecryptResponse) BadRequest() CipherDecryptResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r CipherDecryptResponse) Unauthorized() CipherDecryptResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r CipherDecryptResponse) Internal() CipherDecryptResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r CipherDecryptResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/doc.go000066400000000000000000000010231511163700700211040ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package reqres provides request and response structures for SPIKE mTLS REST // API v1. It includes types for secret operations, policy management, cipher // operations, Shamir secret sharing, operator functions, and bootstrap // procedures. These structures define the contracts for API communication // between clients and SPIKE services. package reqres spike-sdk-go-0.16.4/api/entity/v1/reqres/fallback.go000066400000000000000000000007031511163700700221020ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( "github.com/spiffe/spike-sdk-go/errors" ) // FallbackResponse is a generic response for any error. type FallbackResponse struct { Err errors.ErrorCode `json:"err,omitempty"` } func (r FallbackResponse) ErrorCode() errors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/operator.go000066400000000000000000000040511511163700700221760ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( "github.com/spiffe/spike-sdk-go/api/entity/data" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // RestoreRequest for disaster recovery. type RestoreRequest struct { ID int `json:"id"` Shard *[32]byte `json:"shard"` } // RestoreResponse for disaster recovery. type RestoreResponse struct { data.RestorationStatus Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r RestoreResponse) Success() RestoreResponse { r.Err = "" return r } func (r RestoreResponse) NotFound() RestoreResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r RestoreResponse) BadRequest() RestoreResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r RestoreResponse) Unauthorized() RestoreResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r RestoreResponse) Internal() RestoreResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r RestoreResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // RecoverRequest for disaster recovery. type RecoverRequest struct { } // RecoverResponse for disaster recovery. type RecoverResponse struct { Shards map[int]*[32]byte `json:"shards"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r RecoverResponse) Success() RecoverResponse { r.Err = "" return r } func (r RecoverResponse) NotFound() RecoverResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r RecoverResponse) BadRequest() RecoverResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r RecoverResponse) Unauthorized() RecoverResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r RecoverResponse) Internal() RecoverResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r RecoverResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/policy.go000066400000000000000000000123301511163700700216410ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( "github.com/spiffe/spike-sdk-go/api/entity/data" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // PolicyPutRequest for policy creation. type PolicyPutRequest struct { Name string `json:"name"` SPIFFEIDPattern string `json:"spiffeidPattern"` PathPattern string `json:"pathPattern"` Permissions []data.PolicyPermission `json:"permissions"` } // PolicyPutResponse for policy creation. type PolicyPutResponse struct { ID string `json:"id,omitempty"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r PolicyPutResponse) Success() PolicyPutResponse { r.Err = "" return r } func (r PolicyPutResponse) NotFound() PolicyPutResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r PolicyPutResponse) BadRequest() PolicyPutResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r PolicyPutResponse) Unauthorized() PolicyPutResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r PolicyPutResponse) Internal() PolicyPutResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r PolicyPutResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // PolicyReadRequest to read a policy. type PolicyReadRequest struct { ID string `json:"id"` } // PolicyReadResponse to read a policy. type PolicyReadResponse struct { data.Policy Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r PolicyReadResponse) Success() PolicyReadResponse { r.Err = "" return r } func (r PolicyReadResponse) NotFound() PolicyReadResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r PolicyReadResponse) BadRequest() PolicyReadResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r PolicyReadResponse) Unauthorized() PolicyReadResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r PolicyReadResponse) Internal() PolicyReadResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r PolicyReadResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // PolicyDeleteRequest to delete a policy. type PolicyDeleteRequest struct { ID string `json:"id"` } // PolicyDeleteResponse to delete a policy. type PolicyDeleteResponse struct { Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r PolicyDeleteResponse) Success() PolicyDeleteResponse { r.Err = "" return r } func (r PolicyDeleteResponse) NotFound() PolicyDeleteResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r PolicyDeleteResponse) BadRequest() PolicyDeleteResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r PolicyDeleteResponse) Unauthorized() PolicyDeleteResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r PolicyDeleteResponse) Internal() PolicyDeleteResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r PolicyDeleteResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // PolicyListRequest to list policies. type PolicyListRequest struct { SPIFFEIDPattern string `json:"spiffeidPattern"` PathPattern string `json:"pathPattern"` } // PolicyListResponse to list policies. type PolicyListResponse struct { Policies []data.Policy `json:"policies"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r PolicyListResponse) Success() PolicyListResponse { r.Err = "" return r } func (r PolicyListResponse) NotFound() PolicyListResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r PolicyListResponse) BadRequest() PolicyListResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r PolicyListResponse) Unauthorized() PolicyListResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r PolicyListResponse) Internal() PolicyListResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r PolicyListResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // PolicyAccessCheckRequest to validate policy access. type PolicyAccessCheckRequest struct { SPIFFEID string `json:"spiffeid"` Path string `json:"path"` Action string `json:"action"` } // PolicyAccessCheckResponse to validate policy access. type PolicyAccessCheckResponse struct { Allowed bool `json:"allowed"` MatchingPolicies []string `json:"matchingPolicies"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r PolicyAccessCheckResponse) Success() PolicyAccessCheckResponse { r.Err = "" return r } func (r PolicyAccessCheckResponse) NotFound() PolicyAccessCheckResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r PolicyAccessCheckResponse) BadRequest() PolicyAccessCheckResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r PolicyAccessCheckResponse) Unauthorized() PolicyAccessCheckResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r PolicyAccessCheckResponse) Internal() PolicyAccessCheckResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r PolicyAccessCheckResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/secret.go000066400000000000000000000137431511163700700216400ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( "github.com/spiffe/spike-sdk-go/api/entity/data" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // SecretMetadataRequest for get secrets metadata type SecretMetadataRequest struct { Path string `json:"path"` Version int `json:"version,omitempty"` // Optional specific version } // SecretMetadataResponse for secrets versions and metadata type SecretMetadataResponse struct { data.SecretMetadata Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretMetadataResponse) Success() SecretMetadataResponse { r.Err = "" return r } func (r SecretMetadataResponse) NotFound() SecretMetadataResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r SecretMetadataResponse) BadRequest() SecretMetadataResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretMetadataResponse) Unauthorized() SecretMetadataResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretMetadataResponse) Internal() SecretMetadataResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretMetadataResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // SecretPutRequest for creating/updating secrets type SecretPutRequest struct { Path string `json:"path"` Values map[string]string `json:"values"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } // SecretPutResponse is after a successful secret write operation. type SecretPutResponse struct { Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretPutResponse) Success() SecretPutResponse { r.Err = "" return r } func (r SecretPutResponse) NotFound() SecretPutResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r SecretPutResponse) BadRequest() SecretPutResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretPutResponse) Unauthorized() SecretPutResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretPutResponse) Internal() SecretPutResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretPutResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // SecretGetRequest is for getting secrets type SecretGetRequest struct { Path string `json:"path"` Version int `json:"version,omitempty"` // Optional specific version } // SecretGetResponse is for getting secrets type SecretGetResponse struct { data.Secret Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretGetResponse) Success() SecretGetResponse { r.Err = "" return r } func (r SecretGetResponse) NotFound() SecretGetResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r SecretGetResponse) BadRequest() SecretGetResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretGetResponse) Unauthorized() SecretGetResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretGetResponse) Internal() SecretGetResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretGetResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // SecretDeleteRequest for soft-deleting secret versions type SecretDeleteRequest struct { Path string `json:"path"` Versions []int `json:"versions"` // Empty means the latest version } // SecretDeleteResponse after soft-delete type SecretDeleteResponse struct { Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretDeleteResponse) NotFound() SecretDeleteResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r SecretDeleteResponse) BadRequest() SecretDeleteResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretDeleteResponse) Unauthorized() SecretDeleteResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretDeleteResponse) Internal() SecretDeleteResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretDeleteResponse) Success() SecretDeleteResponse { r.Err = "" return r } func (r SecretDeleteResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // SecretUndeleteRequest for recovering soft-deleted versions type SecretUndeleteRequest struct { Path string `json:"path"` Versions []int `json:"versions"` } // SecretUndeleteResponse after recovery type SecretUndeleteResponse struct { Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretUndeleteResponse) Success() SecretUndeleteResponse { r.Err = "" return r } func (r SecretUndeleteResponse) NotFound() SecretUndeleteResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r SecretUndeleteResponse) BadRequest() SecretUndeleteResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretUndeleteResponse) Unauthorized() SecretUndeleteResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretUndeleteResponse) Internal() SecretUndeleteResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretUndeleteResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // SecretListRequest for listing secrets type SecretListRequest struct { } // SecretListResponse for listing secrets type SecretListResponse struct { Keys []string `json:"keys"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r SecretListResponse) Success() SecretListResponse { r.Err = "" return r } func (r SecretListResponse) NotFound() SecretListResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r SecretListResponse) BadRequest() SecretListResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r SecretListResponse) Unauthorized() SecretListResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r SecretListResponse) Internal() SecretListResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r SecretListResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/entity/v1/reqres/shamir.go000066400000000000000000000045021511163700700216270ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package reqres import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // ShardPutRequest represents a request to submit a shard contribution. // KeeperId specifies the identifier of the keeper responsible for the shard. // Shard represents the shard data being contributed to the system. // Version optionally specifies the version of the shard being submitted. type ShardPutRequest struct { Shard *[32]byte `json:"shard"` } // ShardPutResponse represents the response structure for a shard // contribution. type ShardPutResponse struct { Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r ShardPutResponse) Success() ShardPutResponse { r.Err = "" return r } func (r ShardPutResponse) NotFound() ShardPutResponse { log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid) return r } func (r ShardPutResponse) BadRequest() ShardPutResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r ShardPutResponse) Unauthorized() ShardPutResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r ShardPutResponse) Internal() ShardPutResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r ShardPutResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // ShardGetRequest represents a request to get a Shamir shard. type ShardGetRequest struct { } // ShardGetResponse represents the response that returns a Shamir shard. // The struct includes the shard identifier and an associated error code. type ShardGetResponse struct { Shard *[32]byte `json:"shard"` Err sdkErrors.ErrorCode } func (r ShardGetResponse) Success() ShardGetResponse { r.Err = "" return r } func (r ShardGetResponse) NotFound() ShardGetResponse { r.Err = sdkErrors.ErrAPINotFound.Code return r } func (r ShardGetResponse) BadRequest() ShardGetResponse { r.Err = sdkErrors.ErrAPIBadRequest.Code return r } func (r ShardGetResponse) Unauthorized() ShardGetResponse { r.Err = sdkErrors.ErrAccessUnauthorized.Code return r } func (r ShardGetResponse) Internal() ShardGetResponse { r.Err = sdkErrors.ErrAPIInternal.Code return r } func (r ShardGetResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } spike-sdk-go-0.16.4/api/internal/000077500000000000000000000000001511163700700164655ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/config/000077500000000000000000000000001511163700700177325ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/config/config.go000066400000000000000000000016211511163700700215260ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package config import ( "os" "path/filepath" ) const tmpRootDir = "/tmp" const dataRootDir = "/data" const dataDir = ".spike" // NexusDataFolder returns the path to the directory where Nexus stores // its encrypted backup for its secrets and other data. func NexusDataFolder() string { homeDir, err := os.UserHomeDir() if err != nil { homeDir = tmpRootDir } sd := filepath.Join(homeDir, dataDir) sdr := filepath.Join(sd, dataRootDir) // Create the directory if it doesn't exist // 0700 because we want to restrict access to the directory // but allow the user to create db files in it. err = os.MkdirAll(sdr, 0700) if err != nil { panic(err) } // The data dir is not configurable for security reasons. return sdr } spike-sdk-go-0.16.4/api/internal/impl/000077500000000000000000000000001511163700700174265ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/acl/000077500000000000000000000000001511163700700201655ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/acl/acl_test.go000066400000000000000000000237741511163700700223270ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package acl import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestCreatePolicy_NilSource tests that CreatePolicy returns error for nil X509Source func TestCreatePolicy_NilSource(t *testing.T) { err := CreatePolicy(nil, "test-policy", "spiffe://test/*", "/api/*", []data.PolicyPermission{data.PermissionRead}) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestGetPolicy_NilSource tests that GetPolicy returns error for nil X509Source func TestGetPolicy_NilSource(t *testing.T) { policy, err := GetPolicy(nil, "policy-123") assert.Nil(t, policy) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestDeletePolicy_NilSource tests that DeletePolicy returns error for nil X509Source func TestDeletePolicy_NilSource(t *testing.T) { err := DeletePolicy(nil, "policy-123") assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestListPolicies_NilSource tests that ListPolicies returns error for nil X509Source func TestListPolicies_NilSource(t *testing.T) { policies, err := ListPolicies(nil, "", "") assert.Nil(t, policies) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestPolicyRequestMarshaling tests that policy request structs marshal correctly func TestPolicyRequestMarshaling(t *testing.T) { tests := []struct { name string request interface{} wantErr bool }{ { name: "PolicyPutRequest", request: reqres.PolicyPutRequest{ Name: "test-policy", SPIFFEIDPattern: "spiffe://example.org/*", PathPattern: "/api/*", Permissions: []data.PolicyPermission{data.PermissionRead, data.PermissionWrite}, }, wantErr: false, }, { name: "PolicyReadRequest", request: reqres.PolicyReadRequest{ ID: "policy-123", }, wantErr: false, }, { name: "PolicyDeleteRequest", request: reqres.PolicyDeleteRequest{ ID: "policy-456", }, wantErr: false, }, { name: "PolicyListRequest_Empty", request: reqres.PolicyListRequest{ SPIFFEIDPattern: "", PathPattern: "", }, wantErr: false, }, { name: "PolicyListRequest_WithFilters", request: reqres.PolicyListRequest{ SPIFFEIDPattern: "spiffe://example.org/service/*", PathPattern: "/api/v1/*", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { jsonData, err := json.Marshal(tt.request) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var result map[string]interface{} unmarshalErr := json.Unmarshal(jsonData, &result) assert.NoError(t, unmarshalErr) } }) } } // TestPolicyResponseUnmarshaling tests that policy response structs unmarshal correctly func TestPolicyResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string target interface{} wantErr bool }{ { name: "PolicyPutResponse", jsonData: `{"id":"policy-123"}`, target: &reqres.PolicyPutResponse{}, wantErr: false, }, { name: "PolicyReadResponse", jsonData: `{"id":"policy-123","name":"test-policy","spiffeidPattern":"spiffe://example.org/*","pathPattern":"/api/*","permissions":["read"]}`, target: &reqres.PolicyReadResponse{}, wantErr: false, }, { name: "PolicyDeleteResponse", jsonData: `{}`, target: &reqres.PolicyDeleteResponse{}, wantErr: false, }, { name: "PolicyListResponse_Empty", jsonData: `{"policies":[]}`, target: &reqres.PolicyListResponse{}, wantErr: false, }, { name: "PolicyListResponse_WithPolicies", jsonData: `{"policies":[{"id":"policy-1","name":"test","spiffeidPattern":"spiffe://test/*","pathPattern":"/api/*","permissions":["read"]}]}`, target: &reqres.PolicyListResponse{}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := json.Unmarshal([]byte(tt.jsonData), tt.target) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestPolicyDataConstruction tests that data.Policy is constructed correctly func TestPolicyDataConstruction(t *testing.T) { policy := data.Policy{ ID: "policy-123", Name: "test-policy", SPIFFEIDPattern: "spiffe://example.org/service/*", PathPattern: "/api/documents/*", Permissions: []data.PolicyPermission{data.PermissionRead, data.PermissionWrite}, } assert.Equal(t, "policy-123", policy.ID) assert.Equal(t, "test-policy", policy.Name) assert.Equal(t, "spiffe://example.org/service/*", policy.SPIFFEIDPattern) assert.Equal(t, "/api/documents/*", policy.PathPattern) assert.Equal(t, 2, len(policy.Permissions)) assert.Contains(t, policy.Permissions, data.PermissionRead) assert.Contains(t, policy.Permissions, data.PermissionWrite) } // TestPolicyPermissions tests all policy permission constants func TestPolicyPermissions(t *testing.T) { permissions := []data.PolicyPermission{ data.PermissionRead, data.PermissionWrite, data.PermissionList, data.PermissionExecute, data.PermissionSuper, } // Test that all permissions are distinct seen := make(map[data.PolicyPermission]bool) for _, perm := range permissions { assert.False(t, seen[perm], "Duplicate permission found: %v", perm) seen[perm] = true } // Test that permissions can be marshaled to JSON for _, perm := range permissions { jsonData, err := json.Marshal(perm) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Unmarshal back to verify var unmarshaledPerm data.PolicyPermission err = json.Unmarshal(jsonData, &unmarshaledPerm) assert.NoError(t, err) assert.Equal(t, perm, unmarshaledPerm) } } // TestListPolicies_EmptyResult tests that ListPolicies handles empty results correctly func TestListPolicies_EmptyResult(t *testing.T) { // This test demonstrates the expected behavior for empty results // In practice, the function returns an empty slice pointer when ErrAPINotFound is encountered // Test that we can create an empty policy slice emptyPolicies := []data.Policy{} assert.NotNil(t, emptyPolicies) assert.Equal(t, 0, len(emptyPolicies)) // Test pointer to empty slice emptyPtr := &emptyPolicies assert.NotNil(t, emptyPtr) assert.Equal(t, 0, len(*emptyPtr)) } // TestGetPolicy_NotFoundBehavior tests the expected behavior when policy is not found func TestGetPolicy_NotFoundBehavior(t *testing.T) { // This test demonstrates that GetPolicy returns (nil, nil) for not found // which is the documented behavior for 404 responses // When a policy is not found, both return values should be nil var policy *data.Policy var err *sdkErrors.SDKError assert.Nil(t, policy) assert.Nil(t, err) } // TestPolicyWithMultiplePermissions tests policy with various permission combinations func TestPolicyWithMultiplePermissions(t *testing.T) { testCases := []struct { name string permissions []data.PolicyPermission expected int }{ { name: "SinglePermission", permissions: []data.PolicyPermission{data.PermissionRead}, expected: 1, }, { name: "MultiplePermissions", permissions: []data.PolicyPermission{data.PermissionRead, data.PermissionWrite, data.PermissionList}, expected: 3, }, { name: "AllPermissions", permissions: []data.PolicyPermission{ data.PermissionRead, data.PermissionWrite, data.PermissionList, data.PermissionExecute, data.PermissionSuper, }, expected: 5, }, { name: "NoPermissions", permissions: []data.PolicyPermission{}, expected: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { policy := data.Policy{ ID: "test-policy", Name: tc.name, SPIFFEIDPattern: "spiffe://example.org/*", PathPattern: "/api/*", Permissions: tc.permissions, } assert.Equal(t, tc.expected, len(policy.Permissions)) // Verify JSON marshaling works jsonData, err := json.Marshal(policy) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify unmarshaling works var unmarshaled data.Policy err = json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, err) assert.Equal(t, policy.ID, unmarshaled.ID) assert.Equal(t, len(policy.Permissions), len(unmarshaled.Permissions)) }) } } // TestPolicyPatterns tests various SPIFFE ID and path pattern combinations func TestPolicyPatterns(t *testing.T) { testCases := []struct { name string spiffeIDPattern string pathPattern string valid bool }{ { name: "WildcardBoth", spiffeIDPattern: "spiffe://example.org/*", pathPattern: "/api/*", valid: true, }, { name: "SpecificService", spiffeIDPattern: "spiffe://example.org/service/frontend", pathPattern: "/api/v1/users", valid: true, }, { name: "EmptyPatterns", spiffeIDPattern: "", pathPattern: "", valid: true, }, { name: "ComplexPath", spiffeIDPattern: "spiffe://example.org/ns/*/sa/*", pathPattern: "/api/v1/namespaces/*/secrets/*", valid: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { policy := data.Policy{ ID: "test-policy", Name: tc.name, SPIFFEIDPattern: tc.spiffeIDPattern, PathPattern: tc.pathPattern, Permissions: []data.PolicyPermission{data.PermissionRead}, } // Verify policy can be created assert.NotEmpty(t, policy.ID) // Verify JSON marshaling jsonData, err := json.Marshal(policy) assert.NoError(t, err) assert.NotEmpty(t, jsonData) }) } } spike-sdk-go-0.16.4/api/internal/impl/acl/create.go000066400000000000000000000052741511163700700217670ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package acl import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // CreatePolicy creates a new policy in the system using the provided SPIFFE // X.509 source and policy details. It establishes a mutual TLS connection to // SPIKE Nexus using the X.509 source and sends a policy creation request. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - name: The name of the policy to be created // - SPIFFEIDPattern: The SPIFFE ID pattern that this policy will apply to // - pathPattern: The path pattern that this policy will match against // - permissions: A slice of PolicyPermission defining the access rights // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPIPostFailed: if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error (e.g., // ErrAccessUnauthorized, ErrAPIBadRequest, etc.) // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // permissions := []data.PolicyPermission{ // { // Action: "read", // Resource: "documents/*", // }, // } // // err = CreatePolicy( // source, // "doc-reader", // "spiffe://example.org/service/*", // "/api/documents/*", // permissions, // ) // if err != nil { // log.Printf("Failed to create policy: %v", err) // } func CreatePolicy(source *workloadapi.X509Source, name string, SPIFFEIDPattern string, pathPattern string, permissions []data.PolicyPermission, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } r := reqres.PolicyPutRequest{ Name: name, SPIFFEIDPattern: SPIFFEIDPattern, PathPattern: pathPattern, Permissions: permissions, } var mr []byte mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } _, postErr := net.PostAndUnmarshal[reqres.PolicyPutResponse]( source, url.PolicyCreate(), mr) return postErr } spike-sdk-go-0.16.4/api/internal/impl/acl/delete.go000066400000000000000000000037311511163700700217620ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package acl import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // DeletePolicy removes an existing policy from the system using its ID. // It establishes a mutual TLS connection to SPIKE Nexus using the X.509 source // and sends a policy deletion request. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - id: The unique identifier of the policy to be deleted // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPIPostFailed: if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error (e.g., // ErrAccessUnauthorized, ErrAPINotFound, ErrAPIBadRequest, etc.) // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // err = DeletePolicy(source, "policy-123") // if err != nil { // log.Printf("Failed to delete policy: %v", err) // } func DeletePolicy( source *workloadapi.X509Source, id string, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } r := reqres.PolicyDeleteRequest{ID: id} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } _, postErr := net.PostAndUnmarshal[reqres.PolicyDeleteResponse]( source, url.PolicyDelete(), mr) return postErr } spike-sdk-go-0.16.4/api/internal/impl/acl/doc.go000066400000000000000000000010631511163700700212610ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package acl provides internal implementation for access control list (ACL) // policy management. It includes functions for creating, listing, retrieving, // and deleting policies that control access to SPIKE resources. All operations // require mutual TLS authentication using SPIFFE X.509 certificates and support // predicate-based trust validation for server connections. package acl spike-sdk-go-0.16.4/api/internal/impl/acl/get.go000066400000000000000000000045251511163700700213010ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package acl import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // GetPolicy retrieves a policy from the system using its ID. // It establishes a mutual TLS connection to SPIKE Nexus using the X.509 source // and sends a policy retrieval request. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - id: The unique identifier of the policy to retrieve // // Returns: // - *data.Policy: The policy if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the policy is not found // - ErrAPIPostFailed: if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error (e.g., // ErrAccessUnauthorized, ErrAPIBadRequest, etc.) // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // policy, err := GetPolicy(source, "policy-123") // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // log.Printf("Policy not found") // return // } // log.Printf("Error retrieving policy: %v", err) // return // } // // log.Printf("Found policy: %+v", policy) func GetPolicy( source *workloadapi.X509Source, id string, ) (*data.Policy, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.PolicyReadRequest{ID: id} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.PolicyReadResponse]( source, url.PolicyGet(), mr) if postErr != nil { return nil, postErr } return &res.Policy, nil } spike-sdk-go-0.16.4/api/internal/impl/acl/list.go000066400000000000000000000055421511163700700214750ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package acl import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // ListPolicies retrieves policies from the system, optionally filtering by // SPIFFE ID and path patterns. It establishes a mutual TLS connection to // SPIKE Nexus using the X.509 source and sends a policy list request. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - SPIFFEIDPattern: The SPIFFE ID pattern to filter policies. An empty // string matches all SPIFFE IDs. // - pathPattern: The path pattern to filter policies. An empty string // matches all paths. // // Returns: // - (*[]data.Policy, nil) containing all matching policies if successful // - (nil, nil) if no policies are found // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPIPostFailed: if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error (e.g., // ErrAccessUnauthorized, ErrAPIBadRequest, etc.) // // Note: The returned slice pointer should be dereferenced before use: // // policies := *result // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // // List all policies // result, err := ListPolicies(source, "", "") // if err != nil { // log.Printf("Error listing policies: %v", err) // return // } // if result == nil { // log.Printf("No policies found") // return // } // // policies := *result // for _, policy := range policies { // log.Printf("Found policy: %+v", policy) // } func ListPolicies( source *workloadapi.X509Source, SPIFFEIDPattern string, pathPattern string, ) (*[]data.Policy, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.PolicyListRequest{ SPIFFEIDPattern: SPIFFEIDPattern, PathPattern: pathPattern, } mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.PolicyListResponse]( source, url.PolicyList(), mr) if postErr != nil { if postErr.Is(sdkErrors.ErrAPINotFound) { return &([]data.Policy{}), nil } return nil, postErr } return &res.Policies, nil } spike-sdk-go-0.16.4/api/internal/impl/bootstrap/000077500000000000000000000000001511163700700214435ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/bootstrap/bootstrap.go000066400000000000000000000156021511163700700240130ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package bootstrap import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "github.com/cloudflare/circl/secretsharing" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" "github.com/spiffe/spike-sdk-go/config/env" "github.com/spiffe/spike-sdk-go/crypto" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" "github.com/spiffe/spike-sdk-go/net" "github.com/spiffe/spike-sdk-go/security/mem" ) // Contribute sends a secret share contribution to a SPIKE Keeper during the // bootstrap process. It establishes a mutual TLS connection to the specified // Keeper and transmits the keeper's share of the secret. // // The function marshals the share value, validates its length, and sends it // securely to the Keeper. After sending, the contribution is zeroed out in // memory for security. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Keeper // - keeperShare: The secret share to contribute to the Keeper // - keeperID: The unique identifier of the target Keeper // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - Errors from net.Post(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, // ErrNetPeerConnection) // // Note: The function will fatally crash (via log.FatalErr) for unrecoverable // errors such as marshal failures (ErrDataMarshalFailure) or invalid // contribution length (ErrCryptoInvalidEncryptionKeyLength). // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // err = Contribute(source, keeperShare, "keeper-1") // if err != nil { // log.Printf("Failed to contribute share: %v", err) // } func Contribute( source *workloadapi.X509Source, keeperShare secretsharing.Share, keeperID string, ) *sdkErrors.SDKError { const fName = "Contribute" if source == nil { return sdkErrors.ErrSPIFFENilX509Source } contribution, err := keeperShare.Value.MarshalBinary() if err != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(err) failErr.Msg = "failed to marshal share" log.FatalErr(fName, *failErr) } if len(contribution) != crypto.AES256KeySize { failErr := sdkErrors.ErrCryptoInvalidEncryptionKeyLength failErr.Msg = fmt.Sprintf( "invalid contribution length: expected %d, got %d", crypto.AES256KeySize, len(contribution), ) log.FatalErr(fName, *failErr) } scr := reqres.ShardPutRequest{} shard := new([crypto.AES256KeySize]byte) copy(shard[:], contribution) // Security: Zero out contribution as soon as we don't need it. mem.ClearBytes(contribution) scr.Shard = shard client := net.CreateMTLSClientForKeeper(source) for kid, keeperAPIRoot := range env.KeepersVal() { if kid != keeperID { // These are not the keepers we are looking for... continue } md, marshalErr := json.Marshal(scr) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "failed to marshal request" log.FatalErr(fName, *failErr) } u := url.KeeperBootstrapContributeEndpoint(keeperAPIRoot) _, sdkErr := net.Post(client, u, md) if sdkErr != nil { return sdkErr } } return nil } // Verify performs bootstrap verification with SPIKE Nexus by sending encrypted // random text and validating that Nexus can decrypt it correctly. This ensures // that the bootstrap process completed successfully and Nexus has the correct // master key. // // The function sends the nonce and ciphertext to Nexus, receives back a hash, // and compares it against the expected hash of the original random text. A // match confirms successful bootstrap. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - randomText: The original random text that was encrypted // - nonce: The nonce used during encryption // - ciphertext: The encrypted random text // // Returns: // - *sdkErrors.SDKError: nil on success (hash matches), or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - Errors from net.Post(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, ErrNetPeerConnection) // // Note: The function will fatally crash (via log.FatalErr) for unrecoverable // errors such as marshal failures (ErrDataMarshalFailure), response parsing // failures (ErrDataUnmarshalFailure), or hash verification failures // (ErrCryptoCipherVerificationFailed). These indicate potential security // issues and the application should not continue. // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // err = Verify(source, randomText, nonce, ciphertext) // if err != nil { // log.Printf("Bootstrap verification failed: %v", err) // } func Verify( source *workloadapi.X509Source, randomText string, nonce, ciphertext []byte, ) *sdkErrors.SDKError { const fName = "Verify" if source == nil { return sdkErrors.ErrSPIFFENilX509Source } client := net.CreateMTLSClientForNexus(source) request := reqres.BootstrapVerifyRequest{ Nonce: nonce, Ciphertext: ciphertext, } md, marshalErr := json.Marshal(request) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "failed to marshal verification request" log.FatalErr(fName, *failErr) } // Send the verification request to SPIKE Nexus nexusAPIRoot := env.NexusAPIRootVal() verifyURL := url.NexusBootstrapVerifyEndpoint(nexusAPIRoot) log.Info( fName, "message", "sending verification request to SPIKE Nexus", "url", verifyURL, ) responseBody, err := net.Post(client, verifyURL, md) if err != nil { return err } // Parse the response var verifyResponse struct { Hash string `json:"hash"` Err string `json:"err"` } if unmarshalErr := json.Unmarshal( responseBody, &verifyResponse, ); unmarshalErr != nil { failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr) failErr.Msg = "failed to parse verification response" // If SPIKE Keeper is sending gibberish, it may be a malicious actor. // Fatally crash here to prevent a possible compromise. log.FatalErr(fName, *failErr) } // Compute the expected hash expectedHash := sha256.Sum256([]byte(randomText)) expectedHashHex := hex.EncodeToString(expectedHash[:]) // Verify the hash matches if verifyResponse.Hash != expectedHashHex { failErr := sdkErrors.ErrCryptoCipherVerificationFailed failErr.Msg = "verification failed: hash mismatch" log.FatalErr(fName, *failErr) } return nil } spike-sdk-go-0.16.4/api/internal/impl/bootstrap/bootstrap_test.go000066400000000000000000000301621511163700700250500ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package bootstrap import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestVerify_NilSource tests that Verify returns error for nil X509Source func TestVerify_NilSource(t *testing.T) { randomText := "test random text" nonce := []byte("test nonce") ciphertext := []byte("test ciphertext") err := Verify(nil, randomText, nonce, ciphertext) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestVerify_EmptyRandomText tests Verify with empty random text func TestVerify_EmptyRandomText(t *testing.T) { err := Verify(nil, "", []byte("nonce"), []byte("ciphertext")) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestVerify_NilNonce tests Verify with nil nonce func TestVerify_NilNonce(t *testing.T) { err := Verify(nil, "random text", nil, []byte("ciphertext")) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestVerify_NilCiphertext tests Verify with nil ciphertext func TestVerify_NilCiphertext(t *testing.T) { err := Verify(nil, "random text", []byte("nonce"), nil) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestBootstrapVerifyRequestMarshaling tests that BootstrapVerifyRequest marshals correctly func TestBootstrapVerifyRequestMarshaling(t *testing.T) { tests := []struct { name string request reqres.BootstrapVerifyRequest wantErr bool }{ { name: "ValidRequest", request: reqres.BootstrapVerifyRequest{ Nonce: []byte("test-nonce-12345"), Ciphertext: []byte("encrypted-data-here"), }, wantErr: false, }, { name: "EmptyNonce", request: reqres.BootstrapVerifyRequest{ Nonce: []byte{}, Ciphertext: []byte("encrypted-data"), }, wantErr: false, }, { name: "EmptyCiphertext", request: reqres.BootstrapVerifyRequest{ Nonce: []byte("nonce"), Ciphertext: []byte{}, }, wantErr: false, }, { name: "NilValues", request: reqres.BootstrapVerifyRequest{ Nonce: nil, Ciphertext: nil, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { jsonData, err := json.Marshal(tt.request) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var unmarshaled reqres.BootstrapVerifyRequest unmarshalErr := json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, unmarshalErr) } }) } } // TestBootstrapVerifyResponseUnmarshaling tests that BootstrapVerifyResponse unmarshals correctly func TestBootstrapVerifyResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string wantErr bool }{ { name: "ValidHash", jsonData: `{"hash":"abc123def456","err":""}`, wantErr: false, }, { name: "EmptyHash", jsonData: `{"hash":"","err":""}`, wantErr: false, }, { name: "WithError", jsonData: `{"hash":"","err":"api_internal"}`, wantErr: false, }, { name: "SHA256Hash", jsonData: `{"hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response reqres.BootstrapVerifyResponse err := json.Unmarshal([]byte(tt.jsonData), &response) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestShardPutRequestMarshaling tests that ShardPutRequest marshals correctly func TestShardPutRequestMarshaling(t *testing.T) { shard := &[32]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, } tests := []struct { name string request reqres.ShardPutRequest wantErr bool }{ { name: "ValidShard", request: reqres.ShardPutRequest{ Shard: shard, }, wantErr: false, }, { name: "NilShard", request: reqres.ShardPutRequest{ Shard: nil, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { jsonData, err := json.Marshal(tt.request) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var unmarshaled reqres.ShardPutRequest unmarshalErr := json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, unmarshalErr) } }) } } // TestShardPutResponseUnmarshaling tests that ShardPutResponse unmarshals correctly func TestShardPutResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string wantErr bool }{ { name: "Success", jsonData: `{"err":""}`, wantErr: false, }, { name: "WithError", jsonData: `{"err":"api_bad_request"}`, wantErr: false, }, { name: "EmptyObject", jsonData: `{}`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response reqres.ShardPutResponse err := json.Unmarshal([]byte(tt.jsonData), &response) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestShardGetRequestMarshaling tests that ShardGetRequest marshals correctly func TestShardGetRequestMarshaling(t *testing.T) { request := reqres.ShardGetRequest{} jsonData, err := json.Marshal(request) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var result map[string]interface{} unmarshalErr := json.Unmarshal(jsonData, &result) assert.NoError(t, unmarshalErr) } // TestShardGetResponseUnmarshaling tests that ShardGetResponse unmarshals correctly func TestShardGetResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string wantErr bool }{ { name: "WithShard", jsonData: `{"shard":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]}`, wantErr: false, }, { name: "NullShard", jsonData: `{"shard":null}`, wantErr: false, }, { name: "WithError", jsonData: `{"shard":null,"err":"api_not_found"}`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response reqres.ShardGetResponse err := json.Unmarshal([]byte(tt.jsonData), &response) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestBootstrapVerifyResponseMethods tests the response builder methods func TestBootstrapVerifyResponseMethods(t *testing.T) { tests := []struct { name string method func(reqres.BootstrapVerifyResponse) reqres.BootstrapVerifyResponse expectedCode sdkErrors.ErrorCode }{ { name: "Success", method: reqres.BootstrapVerifyResponse.Success, expectedCode: "", }, { name: "BadRequest", method: reqres.BootstrapVerifyResponse.BadRequest, expectedCode: sdkErrors.ErrAPIBadRequest.Code, }, { name: "Unauthorized", method: reqres.BootstrapVerifyResponse.Unauthorized, expectedCode: sdkErrors.ErrAccessUnauthorized.Code, }, { name: "Internal", method: reqres.BootstrapVerifyResponse.Internal, expectedCode: sdkErrors.ErrAPIInternal.Code, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := reqres.BootstrapVerifyResponse{} result := tt.method(response) assert.Equal(t, tt.expectedCode, result.ErrorCode()) }) } } // TestShardPutResponseMethods tests the response builder methods func TestShardPutResponseMethods(t *testing.T) { tests := []struct { name string method func(reqres.ShardPutResponse) reqres.ShardPutResponse expectedCode sdkErrors.ErrorCode }{ { name: "Success", method: reqres.ShardPutResponse.Success, expectedCode: "", }, { name: "BadRequest", method: reqres.ShardPutResponse.BadRequest, expectedCode: sdkErrors.ErrAPIBadRequest.Code, }, { name: "Unauthorized", method: reqres.ShardPutResponse.Unauthorized, expectedCode: sdkErrors.ErrAccessUnauthorized.Code, }, { name: "Internal", method: reqres.ShardPutResponse.Internal, expectedCode: sdkErrors.ErrAPIInternal.Code, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := reqres.ShardPutResponse{} result := tt.method(response) assert.Equal(t, tt.expectedCode, result.ErrorCode()) }) } } // TestShardGetResponseMethods tests the response builder methods func TestShardGetResponseMethods(t *testing.T) { tests := []struct { name string method func(reqres.ShardGetResponse) reqres.ShardGetResponse expectedCode sdkErrors.ErrorCode }{ { name: "Success", method: reqres.ShardGetResponse.Success, expectedCode: "", }, { name: "NotFound", method: reqres.ShardGetResponse.NotFound, expectedCode: sdkErrors.ErrAPINotFound.Code, }, { name: "BadRequest", method: reqres.ShardGetResponse.BadRequest, expectedCode: sdkErrors.ErrAPIBadRequest.Code, }, { name: "Unauthorized", method: reqres.ShardGetResponse.Unauthorized, expectedCode: sdkErrors.ErrAccessUnauthorized.Code, }, { name: "Internal", method: reqres.ShardGetResponse.Internal, expectedCode: sdkErrors.ErrAPIInternal.Code, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := reqres.ShardGetResponse{} result := tt.method(response) assert.Equal(t, tt.expectedCode, result.ErrorCode()) }) } } // TestShardByteArraySerialization tests 32-byte shard array serialization func TestShardByteArraySerialization(t *testing.T) { // Create a shard with known values shard := &[32]byte{} for i := 0; i < 32; i++ { shard[i] = byte(i + 1) } // Test ShardPutRequest marshaling putRequest := reqres.ShardPutRequest{ Shard: shard, } jsonData, err := json.Marshal(putRequest) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test unmarshaling var unmarshaledPut reqres.ShardPutRequest err = json.Unmarshal(jsonData, &unmarshaledPut) assert.NoError(t, err) assert.NotNil(t, unmarshaledPut.Shard) // Verify shard values for i := 0; i < 32; i++ { assert.Equal(t, byte(i+1), unmarshaledPut.Shard[i]) } // Test ShardGetResponse marshaling getResponse := reqres.ShardGetResponse{ Shard: shard, } jsonData, err = json.Marshal(getResponse) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test unmarshaling var unmarshaledGet reqres.ShardGetResponse err = json.Unmarshal(jsonData, &unmarshaledGet) assert.NoError(t, err) assert.NotNil(t, unmarshaledGet.Shard) // Verify shard values for i := 0; i < 32; i++ { assert.Equal(t, byte(i+1), unmarshaledGet.Shard[i]) } } // TestBootstrapVerifyRequestByteArrays tests byte array handling in BootstrapVerifyRequest func TestBootstrapVerifyRequestByteArrays(t *testing.T) { nonce := make([]byte, 12) for i := 0; i < 12; i++ { nonce[i] = byte(i) } ciphertext := make([]byte, 48) for i := 0; i < 48; i++ { ciphertext[i] = byte(i + 100) } request := reqres.BootstrapVerifyRequest{ Nonce: nonce, Ciphertext: ciphertext, } // Test marshaling jsonData, err := json.Marshal(request) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test unmarshaling var unmarshaled reqres.BootstrapVerifyRequest err = json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, err) // Verify nonce assert.Equal(t, len(nonce), len(unmarshaled.Nonce)) for i := 0; i < len(nonce); i++ { assert.Equal(t, nonce[i], unmarshaled.Nonce[i]) } // Verify ciphertext assert.Equal(t, len(ciphertext), len(unmarshaled.Ciphertext)) for i := 0; i < len(ciphertext); i++ { assert.Equal(t, ciphertext[i], unmarshaled.Ciphertext[i]) } } spike-sdk-go-0.16.4/api/internal/impl/cipher/000077500000000000000000000000001511163700700207005ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/cipher/cipher.go000066400000000000000000000111461511163700700225040ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package cipher import ( "encoding/json" "io" "net/http" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" "github.com/spiffe/spike-sdk-go/net" ) // Cipher encapsulates cipher operations with configurable HTTP client // dependencies. This struct-based approach enables clean dependency injection // for testing without relying on the global mutable state. // // The zero value is not usable; instances should be created using NewCipher(). type Cipher struct { // createMTLSHTTPClientFromSource creates an mTLS HTTP client // from an X509Source createMTLSHTTPClientFromSource func(*workloadapi.X509Source) *http.Client // httpPost performs a POST request and returns the response body httpPost func(*http.Client, string, []byte) ([]byte, *sdkErrors.SDKError) // streamPost performs a streaming POST request with binary data // (always uses application/octet-stream content type) streamPost func( *http.Client, string, io.Reader, ) (io.ReadCloser, *sdkErrors.SDKError) } // NewCipher creates a new Cipher instance with default production dependencies. // The returned Cipher is ready to use for encryption and decryption operations. // // For testing, create a Cipher with custom dependencies by directly // constructing the struct with test doubles. // // Example: // // cipher := NewCipher() // plaintext, err := cipher.Encrypt(source, data, "AES-GCM") func NewCipher() *Cipher { return &Cipher{ createMTLSHTTPClientFromSource: net.CreateMTLSClientForNexus, httpPost: net.Post, streamPost: net.StreamPost, } } // streamOperation performs a streaming encryption or decryption operation. // This is a common helper that removes duplication between EncryptStream // and DecryptStream. // // Parameters: // - source: X509Source for establishing mTLS connection // - r: io.Reader containing the data to process // - urlPath: The API endpoint URL // - fName: Function name for logging purposes // // Returns: // - []byte: The processed data if successful // - *sdkErrors.SDKError: Error if the operation fails func (c *Cipher) streamOperation( source *workloadapi.X509Source, r io.Reader, urlPath string, fName string, ) ([]byte, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } client := c.createMTLSHTTPClientFromSource(source) rc, err := c.streamPost(client, urlPath, r) if err != nil { return nil, err } defer func(rc io.ReadCloser) { if rc == nil { return } closeErr := rc.Close() if closeErr != nil { failErr := sdkErrors.ErrFSStreamCloseFailed.Wrap(closeErr) failErr.Msg = "failed to close response body" log.WarnErr(fName, *failErr) } }(rc) b, readErr := io.ReadAll(rc) if readErr != nil { failErr := sdkErrors.ErrNetReadingResponseBody.Wrap(readErr) failErr.Msg = "failed to read response body" return nil, failErr } return b, nil } // jsonOperation performs a JSON-based operation with generic request/response // handling. This helper removes duplication between Encrypt and Decrypt // operations. // // Parameters: // - source: X509Source for establishing mTLS connection // - request: The request payload (will be marshaled to JSON) // - urlPath: The API endpoint URL // - response: Pointer to response struct that implements ResponseWithError // // Returns: // - *sdkErrors.SDKError: Error if the operation fails, nil on success func (c *Cipher) jsonOperation( source *workloadapi.X509Source, request any, urlPath string, response any, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } client := c.createMTLSHTTPClientFromSource(source) mr, marshalErr := json.Marshal(request) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } body, err := c.httpPost(client, urlPath, mr) if err != nil { return err } if unmarshalErr := json.Unmarshal(body, response); unmarshalErr != nil { failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr) failErr.Msg = "problem parsing response body" return failErr } // Type assertion to check error code // Doing this with generics would be tricky in Go's current type system. if respWithErr, ok := response.(net.ResponseWithError); ok { if errCode := respWithErr.ErrorCode(); errCode != "" { return sdkErrors.FromCode(errCode) } } return nil } spike-sdk-go-0.16.4/api/internal/impl/cipher/decrypt.go000066400000000000000000000142071511163700700227050ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package cipher import ( "io" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // DecryptStream decrypts data from a reader using streaming mode using the // default Cipher instance. // It sends the reader content as the request body and returns the decrypted // plaintext bytes. The data is treated as binary (application/octet-stream) // as decryption operates on raw encrypted bytes. // // This is a convenience function that uses the default Cipher instance. // For testing or custom configuration, create a Cipher instance directly. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - r: io.Reader containing the encrypted data // // Returns: // - ([]byte, nil) containing the decrypted plaintext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // reader := bytes.NewReader(encryptedData) // plaintext, err := DecryptStream(source, reader) // if err != nil { // log.Printf("Decryption failed: %v", err) // } func DecryptStream( source *workloadapi.X509Source, r io.Reader, ) ([]byte, *sdkErrors.SDKError) { return NewCipher().DecryptStream(source, r) } // DecryptStream decrypts data from a reader using streaming mode. // It sends the reader content as the request body and returns the decrypted // plaintext bytes. The data is treated as binary (application/octet-stream) // as decryption operates on raw encrypted bytes. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - r: io.Reader containing the encrypted data // // Returns: // - ([]byte, nil) containing the decrypted plaintext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // cipher := NewCipher() // reader := bytes.NewReader(encryptedData) // plaintext, err := cipher.DecryptStream(source, reader) // if err != nil { // log.Printf("Decryption failed: %v", err) // } func (c *Cipher) DecryptStream( source *workloadapi.X509Source, r io.Reader, ) ([]byte, *sdkErrors.SDKError) { return c.streamOperation(source, r, url.CipherDecrypt(), "DecryptStream") } // Decrypt decrypts data with structured parameters using // the default Cipher instance. // It sends version, nonce, ciphertext, and algorithm and returns // decrypted plaintext bytes. // // This is a convenience function that uses the default Cipher instance. // For testing or custom configuration, create a Cipher instance directly. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - version: The cipher version used during encryption // - nonce: The nonce bytes used during encryption // - ciphertext: The encrypted data to decrypt // - algorithm: The encryption algorithm used (e.g., "AES-GCM") // // Returns: // - ([]byte, nil) containing the decrypted plaintext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, // ErrNetPeerConnection) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // plaintext, err := Decrypt(source, 1, nonce, ciphertext, "AES-GCM") // if err != nil { // log.Printf("Decryption failed: %v", err) // } func Decrypt( source *workloadapi.X509Source, version byte, nonce, ciphertext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { return NewCipher().Decrypt(source, version, nonce, ciphertext, algorithm) } // Decrypt decrypts data with structured parameters. // It sends version, nonce, ciphertext, and algorithm and returns // decrypted plaintext bytes. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - version: The cipher version used during encryption // - nonce: The nonce bytes used during encryption // - ciphertext: The encrypted data to decrypt // - algorithm: The encryption algorithm used (e.g., "AES-GCM") // // Returns: // - ([]byte, nil) containing the decrypted plaintext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, // ErrNetPeerConnection) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // cipher := NewCipher() // plaintext, err := cipher.Decrypt(source, 1, nonce, ciphertext, "AES-GCM") // if err != nil { // log.Printf("Decryption failed: %v", err) // } func (c *Cipher) Decrypt( source *workloadapi.X509Source, version byte, nonce, ciphertext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { payload := reqres.CipherDecryptRequest{ Version: version, Nonce: nonce, Ciphertext: ciphertext, Algorithm: algorithm, } var res reqres.CipherDecryptResponse if err := c.jsonOperation( source, payload, url.CipherDecrypt(), &res, ); err != nil { return nil, err } return res.Plaintext, nil } spike-sdk-go-0.16.4/api/internal/impl/cipher/decrypt_test.go000066400000000000000000000023711511163700700237430ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package cipher import ( "bytes" "io" "net/http" "testing" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) func TestDecrypt_OctetStream(t *testing.T) { // Create a Cipher with test doubles injected cipher := &Cipher{ createMTLSHTTPClientFromSource: func(_ *workloadapi.X509Source) *http.Client { return fakeClient(rtFunc(func(_ *http.Request) (*http.Response, error) { return nil, nil })) }, streamPost: func(_ *http.Client, _ string, body io.Reader) (io.ReadCloser, *sdkErrors.SDKError) { b, _ := io.ReadAll(body) if string(b) != "cipher" { t.Fatalf("unexpected body: %q", string(b)) } return io.NopCloser(bytes.NewReader([]byte("plain"))), nil }, httpPost: func(_ *http.Client, _ string, _ []byte) ([]byte, *sdkErrors.SDKError) { return nil, nil }, } out, err := cipher.DecryptStream( &workloadapi.X509Source{}, bytes.NewReader([]byte("cipher")), ) if err != nil { t.Fatalf("DecryptStream error: %v", err) } if string(out) != "plain" { t.Fatalf("unexpected out: %s", string(out)) } } spike-sdk-go-0.16.4/api/internal/impl/cipher/doc.go000066400000000000000000000010441511163700700217730ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package cipher provides internal implementation for cryptographic operations // including encryption and decryption. It supports two modes of operation: // streaming mode for handling large data efficiently, and JSON mode for // structured request/response communication. All operations require mutual TLS // authentication using SPIFFE X.509 certificates. package cipher spike-sdk-go-0.16.4/api/internal/impl/cipher/encrypt.go000066400000000000000000000134761511163700700227260ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package cipher import ( "io" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // EncryptStream encrypts data from a reader using streaming mode using the // default Cipher instance. // It sends the reader content as the request body and returns the encrypted // ciphertext bytes. The data is treated as binary (application/octet-stream) // as encryption operates on raw bytes. // // This is a convenience function that uses the default Cipher instance. // For testing or custom configuration, create a Cipher instance directly. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - r: io.Reader containing the data to encrypt // // Returns: // - ([]byte, nil) containing the encrypted ciphertext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // reader := bytes.NewReader([]byte("sensitive data")) // ciphertext, err := EncryptStream(source, reader) // if err != nil { // log.Printf("Encryption failed: %v", err) // } func EncryptStream( source *workloadapi.X509Source, r io.Reader, ) ([]byte, *sdkErrors.SDKError) { return NewCipher().EncryptStream(source, r) } // EncryptStream encrypts data from a reader using streaming mode. // It sends the reader content as the request body and returns the encrypted // ciphertext bytes. The data is treated as binary (application/octet-stream) // as encryption operates on raw bytes. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - r: io.Reader containing the data to encrypt // // Returns: // - ([]byte, nil) containing the encrypted ciphertext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - Errors from streamPost(): if the streaming request fails // - ErrNetReadingResponseBody: if reading the response fails // // Example: // // cipher := NewCipher() // reader := bytes.NewReader([]byte("sensitive data")) // ciphertext, err := cipher.EncryptStream(source, reader) // if err != nil { // log.Printf("Encryption failed: %v", err) // } func (c *Cipher) EncryptStream( source *workloadapi.X509Source, r io.Reader, ) ([]byte, *sdkErrors.SDKError) { return c.streamOperation(source, r, url.CipherEncrypt(), "EncryptStream") } // Encrypt encrypts data with structured parameters using // the default Cipher instance. // It sends plaintext and algorithm and returns encrypted ciphertext // bytes. // // This is a convenience function that uses the default Cipher instance. // For testing or custom configuration, create a Cipher instance directly. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - plaintext: The data to encrypt // - algorithm: The encryption algorithm to use (e.g., "AES-GCM") // // Returns: // - ([]byte, nil) containing the encrypted ciphertext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, // ErrNetPeerConnection) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // source, err := workloadapi.NewX509Source(ctx) // if err != nil { // log.Fatal(err) // } // defer source.Close() // // data := []byte("secret message") // ciphertext, err := Encrypt(source, data, "AES-GCM") // if err != nil { // log.Printf("Encryption failed: %v", err) // } func Encrypt( source *workloadapi.X509Source, plaintext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { return NewCipher().Encrypt(source, plaintext, algorithm) } // Encrypt encrypts data with structured parameters. // It sends plaintext and algorithm and returns encrypted ciphertext // bytes. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - plaintext: The data to encrypt // - algorithm: The encryption algorithm to use (e.g., "AES-GCM") // // Returns: // - ([]byte, nil) containing the encrypted ciphertext if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound, // ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, // ErrNetPeerConnection) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // cipher := NewCipher() // data := []byte("secret message") // ciphertext, err := cipher.Encrypt(source, data, "AES-GCM") // if err != nil { // log.Printf("Encryption failed: %v", err) // } func (c *Cipher) Encrypt( source *workloadapi.X509Source, plaintext []byte, algorithm string, ) ([]byte, *sdkErrors.SDKError) { payload := reqres.CipherEncryptRequest{ Plaintext: plaintext, Algorithm: algorithm, } var res reqres.CipherEncryptResponse if err := c.jsonOperation( source, payload, url.CipherEncrypt(), &res, ); err != nil { return nil, err } return res.Ciphertext, nil } spike-sdk-go-0.16.4/api/internal/impl/cipher/encrypt_test.go000066400000000000000000000030321511163700700237500ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package cipher import ( "bytes" "io" "net/http" "testing" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) type rtFunc func(*http.Request) (*http.Response, error) func (f rtFunc) RoundTrip(_ *http.Request) (*http.Response, error) { return f(nil) } func fakeClient(rt http.RoundTripper) *http.Client { return &http.Client{Transport: rt} } func TestEncryptOctetStream(t *testing.T) { // Create a Cipher with test doubles injected cipher := &Cipher{ createMTLSHTTPClientFromSource: func(_ *workloadapi.X509Source) *http.Client { return fakeClient(rtFunc(func(_ *http.Request) (*http.Response, error) { return nil, nil })) }, streamPost: func(_ *http.Client, path string, body io.Reader) (io.ReadCloser, *sdkErrors.SDKError) { if path == "" { t.Fatalf("empty path") } b, _ := io.ReadAll(body) if string(b) != "plain" { t.Fatalf("unexpected body: %q", string(b)) } return io.NopCloser(bytes.NewReader([]byte("cipher"))), nil }, httpPost: func(_ *http.Client, _ string, _ []byte) ([]byte, *sdkErrors.SDKError) { return nil, nil }, } out, err := cipher.EncryptStream( &workloadapi.X509Source{}, bytes.NewReader([]byte("plain")), ) if err != nil { t.Fatalf("EncryptStream error: %v", err) } if string(out) != "cipher" { t.Fatalf("unexpected out: %s", string(out)) } } spike-sdk-go-0.16.4/api/internal/impl/cipher/mode.go000066400000000000000000000007161511163700700221570ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package cipher // Mode selects how encrypt/decrypt requests are made to Nexus. type Mode string const ( // ModeStream encrypts/decrypts data as an io.Reader/io.Writer stream. ModeStream Mode = "stream" // ModeJSON encrypts/decrypts data as a JSON REST request. ModeJSON Mode = "json" ) spike-sdk-go-0.16.4/api/internal/impl/operator/000077500000000000000000000000001511163700700212615ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/operator/doc.go000066400000000000000000000011231511163700700223520ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package operator provides internal implementation for operator-level // functions including recovery and restoration operations using Shamir // secret sharing. // These operations enable disaster recovery scenarios by distributing and // reconstructing cryptographic secrets across multiple shards. All operations // require SPIKE Pilot authentication and use mutual TLS connections to // SPIKE Nexus. package operator spike-sdk-go-0.16.4/api/internal/impl/operator/operator_test.go000066400000000000000000000233341511163700700245070ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package operator import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestRecover_NilSource tests that Recover returns error for nil X509Source func TestRecover_NilSource(t *testing.T) { shards, err := Recover(nil) assert.Nil(t, shards) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestRestore_NilSource tests that Restore returns error for nil X509Source func TestRestore_NilSource(t *testing.T) { shardValue := &[32]byte{} status, err := Restore(nil, 1, shardValue) assert.Nil(t, status) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestRestore_NilShardValue tests that Restore handles nil shard value func TestRestore_NilShardValue(t *testing.T) { status, err := Restore(nil, 1, nil) assert.Nil(t, status) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestRecoverRequestMarshaling tests that RecoverRequest marshals correctly func TestRecoverRequestMarshaling(t *testing.T) { request := reqres.RecoverRequest{} jsonData, err := json.Marshal(request) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var result map[string]interface{} unmarshalErr := json.Unmarshal(jsonData, &result) assert.NoError(t, unmarshalErr) } // TestRestoreRequestMarshaling tests that RestoreRequest marshals correctly func TestRestoreRequestMarshaling(t *testing.T) { shardValue := &[32]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, } tests := []struct { name string request reqres.RestoreRequest wantErr bool }{ { name: "ValidRequest", request: reqres.RestoreRequest{ ID: 1, Shard: shardValue, }, wantErr: false, }, { name: "ZeroID", request: reqres.RestoreRequest{ ID: 0, Shard: shardValue, }, wantErr: false, }, { name: "NilShard", request: reqres.RestoreRequest{ ID: 1, Shard: nil, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { jsonData, err := json.Marshal(tt.request) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify it's valid JSON by unmarshaling back var unmarshaled reqres.RestoreRequest unmarshalErr := json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, unmarshalErr) assert.Equal(t, tt.request.ID, unmarshaled.ID) } }) } } // TestRecoverResponseUnmarshaling tests that RecoverResponse unmarshals correctly func TestRecoverResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string wantErr bool }{ { name: "EmptyShards", jsonData: `{"shards":{}}`, wantErr: false, }, { name: "WithShards", jsonData: `{"shards":{"0":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]}}`, wantErr: false, }, { name: "NullShards", jsonData: `{"shards":null}`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response reqres.RecoverResponse err := json.Unmarshal([]byte(tt.jsonData), &response) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestRestoreResponseUnmarshaling tests that RestoreResponse unmarshals correctly func TestRestoreResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string wantErr bool }{ { name: "InProgress", jsonData: `{"collected":2,"remaining":3,"restored":false}`, wantErr: false, }, { name: "Completed", jsonData: `{"collected":5,"remaining":0,"restored":true}`, wantErr: false, }, { name: "Initial", jsonData: `{"collected":0,"remaining":5,"restored":false}`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response reqres.RestoreResponse err := json.Unmarshal([]byte(tt.jsonData), &response) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } // TestRestorationStatusConstruction tests that RestorationStatus is constructed correctly func TestRestorationStatusConstruction(t *testing.T) { tests := []struct { name string status data.RestorationStatus expected data.RestorationStatus }{ { name: "InProgress", status: data.RestorationStatus{ ShardsCollected: 2, ShardsRemaining: 3, Restored: false, }, expected: data.RestorationStatus{ ShardsCollected: 2, ShardsRemaining: 3, Restored: false, }, }, { name: "Completed", status: data.RestorationStatus{ ShardsCollected: 5, ShardsRemaining: 0, Restored: true, }, expected: data.RestorationStatus{ ShardsCollected: 5, ShardsRemaining: 0, Restored: true, }, }, { name: "Initial", status: data.RestorationStatus{ ShardsCollected: 0, ShardsRemaining: 5, Restored: false, }, expected: data.RestorationStatus{ ShardsCollected: 0, ShardsRemaining: 5, Restored: false, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected.ShardsCollected, tt.status.ShardsCollected) assert.Equal(t, tt.expected.ShardsRemaining, tt.status.ShardsRemaining) assert.Equal(t, tt.expected.Restored, tt.status.Restored) // Verify JSON marshaling works jsonData, err := json.Marshal(tt.status) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Verify unmarshaling works var unmarshaled data.RestorationStatus err = json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, err) assert.Equal(t, tt.status, unmarshaled) }) } } // TestRecoverResponseMethods tests the response builder methods func TestRecoverResponseMethods(t *testing.T) { tests := []struct { name string method func(reqres.RecoverResponse) reqres.RecoverResponse expectedCode sdkErrors.ErrorCode }{ { name: "Success", method: reqres.RecoverResponse.Success, expectedCode: "", }, { name: "BadRequest", method: reqres.RecoverResponse.BadRequest, expectedCode: sdkErrors.ErrAPIBadRequest.Code, }, { name: "Unauthorized", method: reqres.RecoverResponse.Unauthorized, expectedCode: sdkErrors.ErrAccessUnauthorized.Code, }, { name: "Internal", method: reqres.RecoverResponse.Internal, expectedCode: sdkErrors.ErrAPIInternal.Code, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := reqres.RecoverResponse{} result := tt.method(response) assert.Equal(t, tt.expectedCode, result.ErrorCode()) }) } } // TestRestoreResponseMethods tests the response builder methods func TestRestoreResponseMethods(t *testing.T) { tests := []struct { name string method func(reqres.RestoreResponse) reqres.RestoreResponse expectedCode sdkErrors.ErrorCode }{ { name: "Success", method: reqres.RestoreResponse.Success, expectedCode: "", }, { name: "BadRequest", method: reqres.RestoreResponse.BadRequest, expectedCode: sdkErrors.ErrAPIBadRequest.Code, }, { name: "Unauthorized", method: reqres.RestoreResponse.Unauthorized, expectedCode: sdkErrors.ErrAccessUnauthorized.Code, }, { name: "Internal", method: reqres.RestoreResponse.Internal, expectedCode: sdkErrors.ErrAPIInternal.Code, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { response := reqres.RestoreResponse{} result := tt.method(response) assert.Equal(t, tt.expectedCode, result.ErrorCode()) }) } } // TestShardByteArrayHandling tests handling of 32-byte shard arrays func TestShardByteArrayHandling(t *testing.T) { // Create a shard with known values shard := &[32]byte{} for i := 0; i < 32; i++ { shard[i] = byte(i + 1) } // Test JSON marshaling of shard request := reqres.RestoreRequest{ ID: 1, Shard: shard, } jsonData, err := json.Marshal(request) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test unmarshaling var unmarshaled reqres.RestoreRequest err = json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, err) assert.Equal(t, 1, unmarshaled.ID) assert.NotNil(t, unmarshaled.Shard) // Verify shard values for i := 0; i < 32; i++ { assert.Equal(t, byte(i+1), unmarshaled.Shard[i]) } } // TestRecoverShardMapHandling tests handling of shard maps in RecoverResponse func TestRecoverShardMapHandling(t *testing.T) { // Create multiple shards shards := make(map[int]*[32]byte) for i := 0; i < 3; i++ { shard := &[32]byte{} for j := 0; j < 32; j++ { shard[j] = byte(i*32 + j) } shards[i] = shard } response := reqres.RecoverResponse{ Shards: shards, } // Test JSON marshaling jsonData, err := json.Marshal(response) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test unmarshaling var unmarshaled reqres.RecoverResponse err = json.Unmarshal(jsonData, &unmarshaled) assert.NoError(t, err) assert.Equal(t, 3, len(unmarshaled.Shards)) // Verify each shard for i := 0; i < 3; i++ { assert.NotNil(t, unmarshaled.Shards[i]) for j := 0; j < 32; j++ { assert.Equal(t, byte(i*32+j), unmarshaled.Shards[i][j]) } } } spike-sdk-go-0.16.4/api/internal/impl/operator/recover.go000066400000000000000000000107151511163700700232610ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package operator import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" "github.com/spiffe/spike-sdk-go/net" "github.com/spiffe/spike-sdk-go/spiffeid" ) // Recover makes a request to initiate recovery of secrets, returning the // recovery shards. // // SVID Acquisition Error Handling: // // This function attempts to acquire an X.509 SVID from the SPIFFE Workload API // via Unix domain socket. While UDS connections are generally more reliable than // network sockets, SVID acquisition can fail in both fatal and transient ways: // // Fatal failures (indicate misconfiguration): // - Socket file doesn't exist (SPIRE agent never started) // - Permission denied (deployment/configuration error) // - Wrong socket path (configuration error) // // Transient failures (may succeed on retry): // - SPIRE agent restarting (brief unavailability, recovers in seconds) // - SVID not yet provisioned (startup race condition after attestation) // - File descriptor exhaustion (resource pressure may clear) // - SVID rotation failure (temporary SPIRE server issue) // - Workload API connection lost after source creation (agent crash/restart) // - If the SPIFFE provider is SPIRE the workload might not be registered; // or the registration entry might not be propagated through the system yet, // - The workload attestation server, kubelet, or even kubeapi-server might // be overloaded and can't answer the requests from the agent, or it may // even be hard to read data from the /proc/ or the cgroup filesystem. // // Since recovery is often performed during emergency procedures when // infrastructure may be unstable, this function returns errors rather than // crashing to allow retry logic. Callers can implement exponential backoff // or other retry strategies for transient failures. // // Parameters: // - source: X509Source used for mTLS client authentication // // Returns: // - map[int]*[32]byte: Map of shard indices to shard byte arrays if successful // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrSPIFFEFailedToExtractX509SVID: if SVID acquisition fails (may be // transient - see above for retry guidance) // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Security Note: The function will fatally crash (via log.FatalErr) if the // caller is not SPIKE Pilot. This is a programming error, not a runtime // condition, as recovery operations must only be performed by Pilot roles. // // Example: // // shards, err := Recover(x509Source) // if err != nil { // // SVID acquisition failures may be transient - consider retry logic // return nil, err // } func Recover(source *workloadapi.X509Source) ( map[int]*[32]byte, *sdkErrors.SDKError, ) { const fName = "recover" if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } svid, err := source.GetX509SVID() if err != nil { failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err) failErr.Msg = "could not acquire SVID" return nil, failErr } if svid == nil { failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID failErr.Msg = "no X509SVID in source" return nil, failErr } selfSPIFFEID := svid.ID.String() // Security: Recovery and Restoration can ONLY be done via SPIKE Pilot. if !spiffeid.IsPilot(selfSPIFFEID) { failErr := sdkErrors.ErrAccessUnauthorized failErr.Msg = "recovery can only be performed from SPIKE Pilot" log.FatalErr(fName, *failErr) } r := reqres.RecoverRequest{} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "failed to marshal recover request" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.RecoverResponse]( source, url.Recover(), mr) if postErr != nil { return nil, postErr } result := make(map[int]*[32]byte) for i, shard := range res.Shards { result[i] = shard } return result, nil } spike-sdk-go-0.16.4/api/internal/impl/operator/restore.go000066400000000000000000000112371511163700700232770ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package operator import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" "github.com/spiffe/spike-sdk-go/net" "github.com/spiffe/spike-sdk-go/spiffeid" ) // Restore submits a recovery shard to continue the restoration process. // // SVID Acquisition Error Handling: // // This function attempts to acquire an X.509 SVID from the SPIFFE Workload API // via Unix domain socket. While UDS connections are generally more reliable than // network sockets, SVID acquisition can fail in both fatal and transient ways: // // Fatal failures (indicate misconfiguration): // - Socket file doesn't exist (SPIRE agent never started) // - Permission denied (deployment/configuration error) // - Wrong socket path (configuration error) // // Transient failures (may succeed on retry): // - SPIRE agent restarting (brief unavailability, recovers in seconds) // - SVID not yet provisioned (startup race condition after attestation) // - File descriptor exhaustion (resource pressure may clear) // - SVID rotation failure (temporary SPIRE server issue) // - Workload API connection lost after source creation (agent crash/restart) // // Since restoration is often performed during emergency procedures when // infrastructure may be unstable, this function returns errors rather than // crashing to allow retry logic. Callers can implement exponential backoff // or other retry strategies for transient failures. // // Parameters: // - source *workloadapi.X509Source: X509Source used for mTLS client // authentication // - shardIndex int: Index of the recovery shard // - shardValue *[32]byte: Pointer to a 32-byte array containing the recovery // shard // // Returns: // - *data.RestorationStatus: Status containing shards collected, remaining, // and restoration state if successful // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrSPIFFEFailedToExtractX509SVID: if SVID acquisition fails (may be // transient - see above for retry guidance) // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Security Note: The function will fatally crash (via log.FatalErr) if the // caller is not SPIKE Pilot. This is a programming error, not a runtime // condition, as restoration operations must only be performed by Pilot roles. // // Example: // // status, err := Restore(x509Source, shardIndex, shardValue) // if err != nil { // // SVID acquisition failures may be transient - consider retry logic // return nil, err // } func Restore( source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte, ) (*data.RestorationStatus, *sdkErrors.SDKError) { const fName = "restore" if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.RestoreRequest{ID: shardIndex, Shard: shardValue} svid, err := source.GetX509SVID() if err != nil { failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err) failErr.Msg = "could not acquire SVID" return nil, failErr } if svid == nil { failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID failErr.Msg = "no X509SVID in source" return nil, failErr } selfSPIFFEID := svid.ID.String() // Security: Recovery and Restoration can ONLY be done via SPIKE Pilot. if !spiffeid.IsPilot(selfSPIFFEID) { failErr := sdkErrors.ErrAccessUnauthorized failErr.Msg = "restoration can only be performed from SPIKE Pilot" log.FatalErr(fName, *failErr) } mr, marshalErr := json.Marshal(r) // Security: Zero out r.Shard as soon as we're done with it for i := range r.Shard { r.Shard[i] = 0 } if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "failed to marshal restore request" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.RestoreResponse]( source, url.Restore(), mr) // Security: Zero out mr after the POST request is complete for i := range mr { mr[i] = 0 } if postErr != nil { return nil, postErr } return &data.RestorationStatus{ ShardsCollected: res.ShardsCollected, ShardsRemaining: res.ShardsRemaining, Restored: res.Restored, }, nil } spike-sdk-go-0.16.4/api/internal/impl/secret/000077500000000000000000000000001511163700700207135ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/internal/impl/secret/delete.go000066400000000000000000000036001511163700700225030ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // Delete deletes specified versions of a secret at the given path. // // It converts string version numbers to integers, constructs a delete request, // and sends it to the secrets API endpoint. If no versions are specified or // the conversion fails, no versions will be deleted. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - path: Path to the secret to delete // - versions: Integer array of version numbers to delete // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := Delete(x509Source, "secret/path", []int{1, 2}) func Delete( source *workloadapi.X509Source, path string, versions []int, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } r := reqres.SecretDeleteRequest{Path: path, Versions: versions} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } _, postErr := net.PostAndUnmarshal[reqres.SecretDeleteResponse]( source, url.SecretDelete(), mr) return postErr } spike-sdk-go-0.16.4/api/internal/impl/secret/doc.go000066400000000000000000000010221511163700700220020ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package secret provides internal implementation for secret management // operations. It includes functions for creating, retrieving, updating, // deleting, undeleting, and listing secrets, as well as accessing secret // metadata and version information. All operations require mutual TLS // authentication using SPIFFE X.509 certificates. package secret spike-sdk-go-0.16.4/api/internal/impl/secret/get.go000066400000000000000000000036231511163700700220250ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // Get retrieves a specific version of a secret at the given path. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - path: Path to the secret to retrieve // - version: Version number of the secret to retrieve // // Returns: // - *data.Secret: Secret data if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the secret is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // secret, err := Get(x509Source, "secret/path", 1) func Get( source *workloadapi.X509Source, path string, version int, ) (*data.Secret, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.SecretGetRequest{Path: path, Version: version} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.SecretGetResponse]( source, url.SecretGet(), mr) if postErr != nil { return nil, postErr } return &data.Secret{Data: res.Data}, nil } spike-sdk-go-0.16.4/api/internal/impl/secret/list.go000066400000000000000000000034451511163700700222230ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // ListKeys retrieves all secret keys. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // // Returns: // - *[]string: Array of secret keys if found, empty array if no secrets exist // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Note: Returns (*[]string{}, nil) if no secrets are found (ErrAPINotFound) // // Example: // // keys, err := ListKeys(x509Source) func ListKeys( source *workloadapi.X509Source, ) (*[]string, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.SecretListRequest{} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.SecretListResponse]( source, url.SecretList(), mr) if postErr != nil { if postErr.Is(sdkErrors.ErrAPINotFound) { return &[]string{}, nil } return nil, postErr } return &res.Keys, nil } spike-sdk-go-0.16.4/api/internal/impl/secret/metadata_get.go000066400000000000000000000040271511163700700236640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // GetMetadata retrieves a specific version of a secret metadata at the // given path. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - path: Path to the secret to retrieve // - version: Version number of the secret to retrieve // // Returns: // - *data.SecretMetadata: Secret metadata if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the secret metadata is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // metadata, err := GetMetadata(x509Source, "secret/path", 1) func GetMetadata( source *workloadapi.X509Source, path string, version int, ) (*data.SecretMetadata, *sdkErrors.SDKError) { if source == nil { return nil, sdkErrors.ErrSPIFFENilX509Source } r := reqres.SecretMetadataRequest{Path: path, Version: version} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return nil, failErr } res, postErr := net.PostAndUnmarshal[reqres.SecretMetadataResponse]( source, url.SecretMetadataGet(), mr) if postErr != nil { return nil, postErr } return &data.SecretMetadata{ Versions: res.Versions, Metadata: res.Metadata, }, nil } spike-sdk-go-0.16.4/api/internal/impl/secret/put.go000066400000000000000000000033331511163700700220540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // Put creates or updates a secret at the specified path with the given // values. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - path: Path where the secret should be stored // - values: Map of key-value pairs representing the secret data // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := Put(x509Source, "secret/path", // map[string]string{"key": "value"}) func Put( source *workloadapi.X509Source, path string, values map[string]string, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } r := reqres.SecretPutRequest{Path: path, Values: values} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } _, postErr := net.PostAndUnmarshal[reqres.SecretPutResponse]( source, url.SecretPut(), mr) return postErr } spike-sdk-go-0.16.4/api/internal/impl/secret/secret_test.go000066400000000000000000000220651511163700700235730ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestGet_NilSource tests that Get returns error for nil X509Source func TestGet_NilSource(t *testing.T) { secret, err := Get(nil, "test/path", 1) assert.Nil(t, secret) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestPut_NilSource tests that Put returns error for nil X509Source func TestPut_NilSource(t *testing.T) { err := Put(nil, "test/path", map[string]string{"key": "value"}) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestDelete_NilSource tests that Delete returns error for nil X509Source func TestDelete_NilSource(t *testing.T) { err := Delete(nil, "test/path", []int{1}) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestUndelete_NilSource tests that Undelete returns error for nil X509Source func TestUndelete_NilSource(t *testing.T) { err := Undelete(nil, "test/path", []int{1}) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestListKeys_NilSource tests that ListKeys returns error for nil X509Source func TestListKeys_NilSource(t *testing.T) { keys, err := ListKeys(nil) assert.Nil(t, keys) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestGetMetadata_NilSource tests that GetMetadata returns error for nil X509Source func TestGetMetadata_NilSource(t *testing.T) { metadata, err := GetMetadata(nil, "test/path", 1) assert.Nil(t, metadata) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestGet_NotFound tests that Get returns (nil, nil) when secret is not found func TestGet_NotFound(t *testing.T) { // Create a test server that returns 404 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() // Note: This test demonstrates the expected behavior, but without being able // to inject the server URL, it won't actually call the test server. // This is a limitation of the current implementation that could be addressed // with dependency injection. // For now, we test the nil source case which is testable secret, err := Get(nil, "test/path", 1) assert.Nil(t, secret) assert.NotNil(t, err) } // TestListKeys_NotFound tests that ListKeys returns empty array when no secrets exist func TestListKeys_NotFound(t *testing.T) { // Create a test server that returns 404 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() // Note: Same limitation as TestGet_NotFound applies here keys, err := ListKeys(nil) assert.Nil(t, keys) assert.NotNil(t, err) } // TestGetMetadata_NotFound tests that GetMetadata returns (nil, nil) when metadata not found func TestGetMetadata_NotFound(t *testing.T) { // Create a test server that returns 404 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() // Note: Same limitation as TestGet_NotFound applies here metadata, err := GetMetadata(nil, "test/path", 1) assert.Nil(t, metadata) assert.NotNil(t, err) } // TestUndelete_EmptyVersions tests that Undelete handles empty versions array func TestUndelete_EmptyVersions(t *testing.T) { // The function should handle empty versions gracefully // But without a real X509Source, we can only test nil case err := Undelete(nil, "test/path", []int{}) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // Mock response builders for testing func mockSecretGetResponse(secretData map[string]string) reqres.SecretGetResponse { return reqres.SecretGetResponse{ Secret: data.Secret{Data: secretData}, } } func mockSecretListResponse(keys []string) reqres.SecretListResponse { return reqres.SecretListResponse{ Keys: keys, } } func mockSecretMetadataResponse(versions map[int]data.SecretVersionInfo, metadata data.SecretMetaDataContent) reqres.SecretMetadataResponse { return reqres.SecretMetadataResponse{ SecretMetadata: data.SecretMetadata{ Versions: versions, Metadata: metadata, }, } } // Test helper to create mock responses func TestMockResponseBuilders(t *testing.T) { // Test secret get response getResp := mockSecretGetResponse(map[string]string{"key": "value"}) assert.Equal(t, "value", getResp.Data["key"]) // Test secret list response listResp := mockSecretListResponse([]string{"secret1", "secret2"}) assert.Equal(t, 2, len(listResp.Keys)) assert.Equal(t, "secret1", listResp.Keys[0]) // Test secret metadata response now := time.Now() versions := map[int]data.SecretVersionInfo{ 1: {CreatedTime: now, Version: 1, DeletedTime: nil}, } metadata := data.SecretMetaDataContent{ CurrentVersion: 1, OldestVersion: 1, CreatedTime: now, UpdatedTime: now, MaxVersions: 10, } metaResp := mockSecretMetadataResponse(versions, metadata) assert.NotNil(t, metaResp.Versions) assert.Equal(t, 1, metaResp.Metadata.CurrentVersion) assert.Equal(t, 10, metaResp.Metadata.MaxVersions) } // TestDataSecretConstruction tests that data.Secret is constructed correctly func TestDataSecretConstruction(t *testing.T) { secretData := map[string]string{"username": "admin", "password": "secret"} secret := data.Secret{Data: secretData} assert.Equal(t, "admin", secret.Data["username"]) assert.Equal(t, "secret", secret.Data["password"]) } // TestDataSecretMetadataConstruction tests that data.SecretMetadata is constructed correctly func TestDataSecretMetadataConstruction(t *testing.T) { now := time.Now() versions := map[int]data.SecretVersionInfo{ 1: {CreatedTime: now, Version: 1, DeletedTime: nil}, 2: {CreatedTime: now.Add(time.Hour), Version: 2, DeletedTime: nil}, } metadata := data.SecretMetaDataContent{ CurrentVersion: 2, OldestVersion: 1, CreatedTime: now, UpdatedTime: now.Add(time.Hour), MaxVersions: 10, } secretMetadata := data.SecretMetadata{ Versions: versions, Metadata: metadata, } assert.Equal(t, 2, len(secretMetadata.Versions)) assert.Equal(t, 10, secretMetadata.Metadata.MaxVersions) assert.Equal(t, 2, secretMetadata.Metadata.CurrentVersion) } // TestRequestMarshaling tests that request structs marshal correctly to JSON func TestRequestMarshaling(t *testing.T) { tests := []struct { name string request interface{} wantErr bool }{ { name: "SecretGetRequest", request: reqres.SecretGetRequest{ Path: "test/path", Version: 1, }, wantErr: false, }, { name: "SecretPutRequest", request: reqres.SecretPutRequest{ Path: "test/path", Values: map[string]string{"key": "value"}, }, wantErr: false, }, { name: "SecretDeleteRequest", request: reqres.SecretDeleteRequest{ Path: "test/path", Versions: []int{1, 2, 3}, }, wantErr: false, }, { name: "SecretUndeleteRequest", request: reqres.SecretUndeleteRequest{ Path: "test/path", Versions: []int{1, 2}, }, wantErr: false, }, { name: "SecretListRequest", request: reqres.SecretListRequest{}, wantErr: false, }, { name: "SecretMetadataRequest", request: reqres.SecretMetadataRequest{ Path: "test/path", Version: 1, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := json.Marshal(tt.request) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, data) // Verify it's valid JSON by unmarshaling back var result map[string]interface{} unmarshalErr := json.Unmarshal(data, &result) assert.NoError(t, unmarshalErr) } }) } } // TestResponseUnmarshaling tests that response structs unmarshal correctly from JSON func TestResponseUnmarshaling(t *testing.T) { tests := []struct { name string jsonData string target interface{} wantErr bool }{ { name: "SecretGetResponse", jsonData: `{"data":{"username":"admin","password":"secret"}}`, target: &reqres.SecretGetResponse{}, wantErr: false, }, { name: "SecretListResponse", jsonData: `{"keys":["secret1","secret2","secret3"]}`, target: &reqres.SecretListResponse{}, wantErr: false, }, { name: "SecretMetadataResponse", jsonData: `{"versions":{"1":{"created_time":"2024-01-01"}},"metadata":{"max_versions":10}}`, target: &reqres.SecretMetadataResponse{}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := json.Unmarshal([]byte(tt.jsonData), tt.target) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } spike-sdk-go-0.16.4/api/internal/impl/secret/undelete.go000066400000000000000000000035011511163700700230460ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package secret import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike-sdk-go/api/entity/v1/reqres" "github.com/spiffe/spike-sdk-go/api/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/net" ) // Undelete restores previously deleted versions of a secret at the // specified path. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - path: Path to the secret to restore // - versions: Integer array of version numbers to restore. Empty array // attempts no restoration // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := Undelete(x509Source, "secret/path", []int{1, 2}) func Undelete(source *workloadapi.X509Source, path string, versions []int, ) *sdkErrors.SDKError { if source == nil { return sdkErrors.ErrSPIFFENilX509Source } var vv []int if len(versions) == 0 { vv = []int{} } else { vv = versions } r := reqres.SecretUndeleteRequest{Path: path, Versions: vv} mr, marshalErr := json.Marshal(r) if marshalErr != nil { failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr) failErr.Msg = "problem generating the payload" return failErr } _, postErr := net.PostAndUnmarshal[reqres.SecretUndeleteResponse]( source, url.SecretUndelete(), mr) return postErr } spike-sdk-go-0.16.4/api/policy.go000066400000000000000000000115721511163700700165050ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/internal/impl/acl" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // CreatePolicy creates a new policy in the system. // // It establishes a mutual TLS connection using the X.509 source and sends a // policy creation request to SPIKE Nexus. // // Parameters: // - name: The name of the policy to be created // - SPIFFEIDPattern: The SPIFFE ID pattern that this policy will apply to // - pathPattern: The path pattern that this policy will match against // - permissions: A slice of PolicyPermission defining the access rights // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // permissions := []data.PolicyPermission{ // {Action: "read", Resource: "documents/*"}, // } // err := api.CreatePolicy( // "doc-reader", // "spiffe://example.org/service/*", // "/api/documents/*", // permissions, // ) // if err != nil { // log.Printf("Failed to create policy: %v", err) // } func (a *API) CreatePolicy( name string, SPIFFEIDPattern string, pathPattern string, permissions []data.PolicyPermission, ) *sdkErrors.SDKError { return acl.CreatePolicy(a.source, name, SPIFFEIDPattern, pathPattern, permissions) } // DeletePolicy removes an existing policy from the system using its unique ID. // // Parameters: // - id: The unique identifier of the policy to be deleted // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := api.DeletePolicy("policy-123") // if err != nil { // log.Printf("Failed to delete policy: %v", err) // } func (a *API) DeletePolicy(id string) *sdkErrors.SDKError { return acl.DeletePolicy(a.source, id) } // GetPolicy retrieves a policy from the system using its unique ID. // // Parameters: // - id: The unique identifier of the policy to retrieve // // Returns: // - *data.Policy: The policy if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the policy is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // policy, err := api.GetPolicy("policy-123") // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // log.Printf("Policy not found") // return // } // log.Printf("Error retrieving policy: %v", err) // return // } // log.Printf("Found policy: %+v", policy) func (a *API) GetPolicy(id string) (*data.Policy, *sdkErrors.SDKError) { return acl.GetPolicy(a.source, id) } // ListPolicies retrieves policies from the system, optionally filtering by // SPIFFE ID and path patterns. // // Parameters: // - SPIFFEIDPattern: The SPIFFE ID pattern to filter policies (empty string // matches all SPIFFE IDs) // - pathPattern: The path pattern to filter policies (empty string matches // all paths) // // Returns: // - *[]data.Policy: Array of matching policies, empty array if none found, // nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Note: Returns (&[]data.Policy{}, nil) if no policies are found (ErrAPINotFound) // // Example: // // result, err := api.ListPolicies("", "") // if err != nil { // log.Printf("Error listing policies: %v", err) // return // } // policies := *result // for _, policy := range policies { // log.Printf("Found policy: %+v", policy) // } func (a *API) ListPolicies( SPIFFEIDPattern, pathPattern string, ) (*[]data.Policy, *sdkErrors.SDKError) { return acl.ListPolicies(a.source, SPIFFEIDPattern, pathPattern) } spike-sdk-go-0.16.4/api/recovery.go000066400000000000000000000060351511163700700170420ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/internal/impl/operator" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // Recover returns recovery partitions for SPIKE Nexus to be used in a // break-the-glass recovery operation. // // This should be used when the SPIKE Nexus auto-recovery mechanism isn't // successful. The returned shards are sensitive and should be securely stored // out-of-band in encrypted form. // // Returns: // - map[int]*[32]byte: Map of shard indices to shard byte arrays if // successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Note: The function will fatally crash (via log.FatalErr) if: // - SVID acquisition fails // - SVID is nil // - Caller is not SPIKE Pilot (security requirement) // // Example: // // shards, err := api.Recover() // if err != nil { // log.Fatalf("Failed to recover shards: %v", err) // } func (a *API) Recover() (map[int]*[32]byte, *sdkErrors.SDKError) { return operator.Recover(a.source) } // Restore submits a recovery shard to continue the SPIKE Nexus restoration // process. // // This is used when SPIKE Keepers cannot provide adequate shards and SPIKE // Nexus cannot recall its root key. This is a break-the-glass superuser-only // operation that a well-architected SPIKE deployment should not need. // // Parameters: // - index: Index of the recovery shard // - shard: Pointer to a 32-byte array containing the recovery shard // // Returns: // - *data.RestorationStatus: Status containing shards collected, remaining, // and restoration state if successful, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Note: The function will fatally crash (via log.FatalErr) if: // - SVID acquisition fails // - SVID is nil // - Caller is not SPIKE Pilot (security requirement) // // Example: // // status, err := api.Restore(shardIndex, shardPtr) // if err != nil { // log.Fatalf("Failed to restore shard: %v", err) // } // log.Printf("Shards collected: %d, remaining: %d", // status.ShardsCollected, status.ShardsRemaining) func (a *API) Restore( index int, shard *[32]byte, ) (*data.RestorationStatus, *sdkErrors.SDKError) { return operator.Restore(a.source, index, shard) } spike-sdk-go-0.16.4/api/secret.go000066400000000000000000000176741511163700700165040ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\ SPDX-License-Identifier: Apache-2.0 package api import ( "github.com/spiffe/spike-sdk-go/api/entity/data" "github.com/spiffe/spike-sdk-go/api/internal/impl/secret" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // DeleteSecretVersions deletes specified versions of a secret at the given // path. // // Parameters: // - path: Path to the secret to delete // - versions: Array of version numbers to delete // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := api.DeleteSecretVersions("secret/path", []int{1, 2}) // if err != nil { // log.Printf("Failed to delete secret versions: %v", err) // } func (a *API) DeleteSecretVersions( path string, versions []int, ) *sdkErrors.SDKError { return secret.Delete(a.source, path, versions) } // DeleteSecret deletes the entire secret at the given path. // // Parameters: // - path: Path to the secret to delete // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := api.DeleteSecret("secret/path") // if err != nil { // log.Printf("Failed to delete secret: %v", err) // } func (a *API) DeleteSecret(path string) *sdkErrors.SDKError { return secret.Delete(a.source, path, []int{}) } // GetSecretVersion retrieves a specific version of a secret at the given // path. // // Parameters: // - path: Path to the secret to retrieve // - version: Version number of the secret to retrieve // // Returns: // - *data.Secret: Secret data if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the secret is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // secret, err := api.GetSecretVersion("secret/path", 1) // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // log.Printf("Secret not found") // return // } // log.Printf("Error retrieving secret: %v", err) // return // } func (a *API) GetSecretVersion( path string, version int, ) (*data.Secret, *sdkErrors.SDKError) { return secret.Get(a.source, path, version) } // GetSecret retrieves the latest version of the secret at the given path. // // Parameters: // - path: Path to the secret to retrieve // // Returns: // - *data.Secret: Secret data if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the secret is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // secret, err := api.GetSecret("secret/path") // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // log.Printf("Secret not found") // return // } // log.Printf("Error retrieving secret: %v", err) // return // } func (a *API) GetSecret(path string) (*data.Secret, *sdkErrors.SDKError) { return secret.Get(a.source, path, 0) } // ListSecretKeys retrieves all secret keys. // // Returns: // - *[]string: Array of secret keys if found, empty array if none found, // nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound) // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Note: Returns (&[]string{}, nil) if no secrets are found (ErrAPINotFound) // // Example: // // keys, err := api.ListSecretKeys() // if err != nil { // log.Printf("Error listing keys: %v", err) // return // } // for _, key := range *keys { // log.Printf("Found key: %s", key) // } func (a *API) ListSecretKeys() (*[]string, *sdkErrors.SDKError) { return secret.ListKeys(a.source) } // GetSecretMetadata retrieves metadata for a specific version of a secret at // the given path. // // Parameters: // - path: Path to the secret to retrieve metadata for // - version: Version number of the secret to retrieve metadata for // // Returns: // - *data.SecretMetadata: Secret metadata if found, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - ErrAPINotFound: if the secret metadata is not found // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // metadata, err := api.GetSecretMetadata("secret/path", 1) // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // log.Printf("Metadata not found") // return // } // log.Printf("Error retrieving metadata: %v", err) // return // } func (a *API) GetSecretMetadata( path string, version int, ) (*data.SecretMetadata, *sdkErrors.SDKError) { return secret.GetMetadata(a.source, path, version) } // PutSecret creates or updates a secret at the specified path with the given // values. // // Parameters: // - path: Path where the secret should be stored // - data: Map of key-value pairs representing the secret data // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := api.PutSecret("secret/path", map[string]string{"key": "value"}) // if err != nil { // log.Printf("Failed to put secret: %v", err) // } func (a *API) PutSecret( path string, data map[string]string, ) *sdkErrors.SDKError { return secret.Put(a.source, path, data) } // UndeleteSecret restores previously deleted versions of a secret at the // specified path. // // Parameters: // - path: Path to the secret to restore // - versions: Array of version numbers to restore (empty array attempts no // restoration) // // Returns: // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrSPIFFENilX509Source: if the X509 source is nil // - ErrDataMarshalFailure: if request serialization fails // - Errors from net.Post(): if the HTTP request fails // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the server returns an error // // Example: // // err := api.UndeleteSecret("secret/path", []int{1, 2}) // if err != nil { // log.Printf("Failed to undelete secret: %v", err) // } func (a *API) UndeleteSecret(path string, versions []int) *sdkErrors.SDKError { return secret.Undelete(a.source, path, versions) } spike-sdk-go-0.16.4/api/url/000077500000000000000000000000001511163700700154535ustar00rootroot00000000000000spike-sdk-go-0.16.4/api/url/bootstrap.go000066400000000000000000000044221511163700700200210ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // KeeperBootstrapContributeEndpoint constructs the full API endpoint URL for // SPIKE Keeper contribution requests. // // It joins the provided keeper API root URL with the KeeperContribute path // segment to create a complete endpoint URL for submitting secret shares to // keepers. // // Parameters: // - keeperAPIRoot: The base URL of the SPIKE Keeper API // // Returns: // - string: The complete endpoint URL for keeper contribution requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := KeeperBootstrapContributeEndpoint("https://keeper.example.com") func KeeperBootstrapContributeEndpoint(keeperAPIRoot string) string { const fName = "KeeperBootstrapContributeEndpoint" u, err := url.JoinPath( keeperAPIRoot, string(KeeperContribute), ) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Keeper API path" log.FatalErr(fName, *failErr) } return u } // NexusBootstrapVerifyEndpoint constructs the full API endpoint URL for // bootstrap verification requests. // // It joins the provided Nexus API root URL with the bootstrap verify path to // create a complete endpoint URL for verifying that SPIKE Nexus has been // properly initialized with the root key. // // Parameters: // - nexusAPIRoot: The base URL of the SPIKE Nexus API // // Returns: // - string: The complete endpoint URL for bootstrap verification requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := NexusBootstrapVerifyEndpoint("https://nexus.example.com") func NexusBootstrapVerifyEndpoint(nexusAPIRoot string) string { const fName = "NexusBootstrapVerifyEndpoint" u, err := url.JoinPath(nexusAPIRoot, string(NexusBootstrapVerify)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus API path" log.FatalErr(fName, *failErr) } return u } spike-sdk-go-0.16.4/api/url/cipher.go000066400000000000000000000036071511163700700172620ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // CipherEncrypt constructs the full API endpoint URL for encryption requests. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the cipher encrypt path to create a complete endpoint URL for encrypting // data. // // Returns: // - string: The complete endpoint URL for encryption requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := CipherEncrypt() func CipherEncrypt() string { const fName = "CipherEncrypt" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusCipherEncrypt)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus cipher encrypt path" log.FatalErr(fName, *failErr) } return u } // CipherDecrypt constructs the full API endpoint URL for decryption requests. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the cipher decrypt path to create a complete endpoint URL for decrypting // data. // // Returns: // - string: The complete endpoint URL for decryption requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := CipherDecrypt() func CipherDecrypt() string { const fName = "CipherDecrypt" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusCipherDecrypt)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus cipher decrypt path" log.FatalErr(fName, *failErr) } return u } spike-sdk-go-0.16.4/api/url/config.go000066400000000000000000000022171511163700700172510ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url type APIAction string const KeyAPIAction = "action" const ActionCheck APIAction = "check" const ActionGet APIAction = "get" const ActionDelete APIAction = "delete" const ActionUndelete APIAction = "undelete" const ActionList APIAction = "list" const ActionDefault APIAction = "" const ActionRead APIAction = "read" type APIURL string const NexusSecrets APIURL = "/v1/store/secrets" const NexusSecretsMetadata APIURL = "/v1/store/secrets/metadata" const NexusInit APIURL = "/v1/auth/initialization" const NexusPolicy APIURL = "/v1/acl/policy" const NexusOperatorRecover APIURL = "/v1/operator/recover" const NexusOperatorRestore APIURL = "/v1/operator/restore" const NexusCipherEncrypt APIURL = "/v1/cipher/encrypt" const NexusCipherDecrypt APIURL = "/v1/cipher/decrypt" const NexusBootstrapVerify APIURL = "/v1/bootstrap/verify" const KeeperKeep APIURL = "/v1/store/keep" const KeeperShard APIURL = "/v1/store/shard" const KeeperContribute APIURL = "/v1/store/contribute" spike-sdk-go-0.16.4/api/url/doc.go000066400000000000000000000011651511163700700165520ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package url provides URL construction utilities for SPIKE API endpoints. // It defines constants for API actions and endpoint paths, and provides // functions to construct complete URLs for various operations including // secret management, policy operations, cipher functions, operator tasks, // and token handling. URLs are constructed by combining base URLs from // the environment configuration with endpoint paths and query parameters. package url spike-sdk-go-0.16.4/api/url/operator.go000066400000000000000000000037241511163700700176430ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // Restore constructs the full API endpoint URL for operator restore requests. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the operator restore path to create a complete endpoint URL for submitting // recovery shards during the restoration process. // // Returns: // - string: The complete endpoint URL for operator restore requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := Restore() func Restore() string { const fName = "Restore" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusOperatorRestore)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus operator restore path" log.FatalErr(fName, *failErr) } return u } // Recover constructs the full API endpoint URL for operator recover requests. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the operator recover path to create a complete endpoint URL for initiating // the recovery process and retrieving recovery shards. // // Returns: // - string: The complete endpoint URL for operator recover requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := Recover() func Recover() string { const fName = "Recover" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusOperatorRecover)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus operator recover path" log.FatalErr(fName, *failErr) } return u } spike-sdk-go-0.16.4/api/url/policy.go000066400000000000000000000071241511163700700173050ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // PolicyCreate constructs the full API endpoint URL for creating policies. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the policy path to create a complete endpoint URL for creating new policies. // // Returns: // - string: The complete endpoint URL for policy creation requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := PolicyCreate() func PolicyCreate() string { const fName = "PolicyCreate" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus policy path" log.FatalErr(fName, *failErr) } return u } // PolicyList constructs the full API endpoint URL for listing policies. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the policy path and adds query parameters to specify the list action. // // Returns: // - string: The complete endpoint URL for policy list requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := PolicyList() func PolicyList() string { const fName = "PolicyList" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus policy path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionList)) return u + "?" + params.Encode() } // PolicyDelete constructs the full API endpoint URL for deleting policies. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the policy path and adds query parameters to specify the delete action. // // Returns: // - string: The complete endpoint URL for policy deletion requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := PolicyDelete() func PolicyDelete() string { const fName = "PolicyDelete" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus policy path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionDelete)) return u + "?" + params.Encode() } // PolicyGet constructs the full API endpoint URL for retrieving a policy. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the policy path and adds query parameters to specify the get action. // // Returns: // - string: The complete endpoint URL for policy retrieval requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := PolicyGet() func PolicyGet() string { const fName = "PolicyGet" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus policy path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionGet)) return u + "?" + params.Encode() } spike-sdk-go-0.16.4/api/url/secret.go000066400000000000000000000125721511163700700172760ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // SecretGet constructs the full API endpoint URL for retrieving secrets. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets path and adds query parameters to specify the get action. // // Returns: // - string: The complete endpoint URL for secret retrieval requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretGet() func SecretGet() string { const fName = "SecretGet" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionGet)) return u + "?" + params.Encode() } // SecretPut constructs the full API endpoint URL for creating or updating // secrets. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets path to create a complete endpoint URL for storing secrets. // // Returns: // - string: The complete endpoint URL for secret creation/update requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretPut() func SecretPut() string { const fName = "SecretPut" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets path" log.FatalErr(fName, *failErr) } return u } // SecretDelete constructs the full API endpoint URL for deleting secrets. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets path and adds query parameters to specify the delete action. // // Returns: // - string: The complete endpoint URL for secret deletion requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretDelete() func SecretDelete() string { const fName = "SecretDelete" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionDelete)) return u + "?" + params.Encode() } // SecretUndelete constructs the full API endpoint URL for restoring deleted // secrets. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets path and adds query parameters to specify the undelete action. // // Returns: // - string: The complete endpoint URL for secret restoration requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretUndelete() func SecretUndelete() string { const fName = "SecretUndelete" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionUndelete)) return u + "?" + params.Encode() } // SecretList constructs the full API endpoint URL for listing secrets. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets path and adds query parameters to specify the list action. // // Returns: // - string: The complete endpoint URL for secret list requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretList() func SecretList() string { const fName = "SecretList" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionList)) return u + "?" + params.Encode() } // SecretMetadataGet constructs the full API endpoint URL for retrieving secret // metadata. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the secrets metadata path and adds query parameters to specify the get action. // // Returns: // - string: The complete endpoint URL for secret metadata retrieval requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := SecretMetadataGet() func SecretMetadataGet() string { const fName = "SecretMetadataGet" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecretsMetadata)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus secrets metadata path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionGet)) return u + "?" + params.Encode() } spike-sdk-go-0.16.4/api/url/token.go000066400000000000000000000040161511163700700171230ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package url import ( "net/url" "github.com/spiffe/spike-sdk-go/config/env" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // Init constructs the full API endpoint URL for initializing SPIKE Nexus. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the init path to create a complete endpoint URL for initializing SPIKE Nexus // with the root encryption key. // // Returns: // - string: The complete endpoint URL for initialization requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := Init() func Init() string { const fName = "Init" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusInit)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus init path" log.FatalErr(fName, *failErr) } return u } // InitState constructs the full API endpoint URL for checking the // initialization state of SPIKE Nexus. // // It joins the SPIKE Nexus API root URL (from environment configuration) with // the init path and adds query parameters to specify the check action for // verifying whether SPIKE Nexus has been initialized. // // Returns: // - string: The complete endpoint URL for initialization state check requests // // Note: The function will fatally crash (via log.FatalErr) if URL path joining // fails. // // Example: // // endpoint := InitState() func InitState() string { const fName = "InitState" u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusInit)) if err != nil { failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err) failErr.Msg = "failed to join SPIKE Nexus init path" log.FatalErr(fName, *failErr) } params := url.Values{} params.Add(KeyAPIAction, string(ActionCheck)) return u + "?" + params.Encode() } spike-sdk-go-0.16.4/assets/000077500000000000000000000000001511163700700154025ustar00rootroot00000000000000spike-sdk-go-0.16.4/assets/spike-banner-lg.png000066400000000000000000000770651511163700700211050ustar00rootroot00000000000000PNG  IHDRsRGBDeXIfMM*i٠W@IDATx]|^H(!tE"*]TWEw^~v~6""RҐ{ os\BH.O^̛{͌]04>gF`F`|H/M Ca=,gpFcȐxK/5((`F`Fn"P5 *B ^-z:#"dûx/d}gbF`Fi%5 7ǎ4vQ IIɾRv+]BlE)QF`F`:@V5*rRBe#ImQEl[ttFqX 3#0#M괒jdͻ[KN4a-"## ~-Pz-`F`F&uZƒKvYoV`NN4ѤoQ7\(i2+p`F`FN#PworHKl(Bc݄LNNJ'9`F`FY%SQxSSlr_1ꫯv'''I ;O8#0#0@F֩ج LW\Ԃ\ rvky u 2#0#PJXS),X //LII^{ 3#0#}ꮒ-2-;=Ws _!f}qFa!Y)QF`F`z@T#אj.mWR>#Xs|$ŵz(QR#0#0 df8t>*5ӣŊ=bĈݻw/#gp`F`F~!Pld%M^tj,Jvڍ$5ؤ3f42Bh?&90#0#PsJЬ nBCzaW?Mh.Mqxǒ490#0#PВ83"EBӦ+(=mROmBs,W޽ 8PΚڰջ,F`F`B@JЧKtDdz4M/xRd9r n.FExǚQvMcƱ\>/6e=BF`F`?(>UχCرg"J CцaV(FeCQY5d/)XΝ 6LVV?s`F`F^#P%[SEy.{Y/#݄F6c +!_rOOnBS"#~,VR-g=,'80#0#PPɆhUeDvɉPǝyGO^aq) %v忄7o7qDSr_ks0#0#02#dUu  ^fHA]<#16҇aզ]F`F`9B"6|mB?1n5OtcZ֓/8l?ҵ.:^ѴEv֡1#0#<,fW٤uM7,wfE'R a鱆7!+e|HÂ#P!vP?Y*~RKbF`F `*z ?<\ ܦ輭Ѿ](qqFpc߀>c NM 'UQ>L{"lZ犏2i+n)1 M.*7rPx׫(g= QE9m9E:SIdZQ^Դ?#0#0urlh_ŖqXæd U,7 >_pY/No& 򒑨 N[B5MIouɽe<?JaF`F2 kiC~"5eV͢di<@؝P ח yW^YܸBv-U. 1cY"S YF`F`*e-ٖaɆ%oa:F_vsw.,'3Rw!8eiCm#S`&8#0#0@(W~hNmXW at!\r͛YBQI5N?e6 u_gYgthrBŷBYr=3#0#TJlMnᴙKCSvX|Za?cv EcádOD壘ͦ{qLɟi)]cor:D*/!e5Ʊ<'[IF0;fF`F#`Xq&kJfӞ) -.,"E /#'iFz69&bR?D߃"|V쓳_̖qɅgF`Fh[MӚ:D{! 5Qr(rFfO]=}FyM&FY)?^!)Wm,n QȬu90#0#\*B,=-WwLXIvɣ?ptF>i3f̐IQ}(0=(9wԙXqF`F`#PɆ ٲrM,mBsB|L }ې!CCΚ 5[w<=p!:W9{!/-50#0#b*eeUQ-&FɎKVOL:/<),kS" "l'gGb=s`F`@`%[5.hHnn;95G<p{Me!V9#0#0ETnaqq-ۇޒ=y nDn"[7( Am&Vl*huCOiCce_y{xˑe'J~(JneF`F`TEr%as٘6ѩmLR^(-={7hРAJ/ͺ 5Ώd-y3TvY? BᒍaF`F(EÌ mElMh6!0d r 퍗|'ኧg7k/4e-5}؝w2 p0#0#XhɆkd;BnɆ"i?>JfA' 8-g51BnGxƪ&)-DsvI7n8YVR\#0#0EMsCZMJ z%[O-&=U} ?ؖQv+vƺ^}>>FE%7Ų&}F`F`@%;i]Sh; 8dBdGfiE#m6oP)ٷuǍgZQ YPǿښ-.+4nL~福y ^$#0#0@ `*FBheEuCV^,Ah&=X.ǃu㡴Yab/Y"QX$YFQ򺧼'yPs!F`F`j2JfU eJqѾ{`eAOmK⥤M4IƆ~6y&lRAe)ck.˗wQ q&#0#0@ +%(ZSYp/˭EJW/fhcΌ3bvsf_⇅夼Ε;vx(gM#0#0(d+ߒmYY$}Cd/Y~}Ǿ E}&%%_tEP 3=ibOY~s6xN/ q`F`FQ.G M GKFOt]f.Y &4IϢ>N9B4sj -j~l%mu˺86),g0#0#8elmXCd+6*2QEZdӒ}Y?8eύ(Q0=+EhO}mQ*6BF4(yr&F`F`jt$۱`6U&4'G/X2t;Iwu#2ѧY9˙(wϢb\6#12<=ֿ8$a|<"F`F(2J6 1 6Yי]O@vӮJ;E/nt) 98_m}ҕ"TO|<[I nZb4{XzTDh.i8uu±*LJ0#HUTdNausͭ?&NEQ-ٿIr+J =d?6>ϋ44Z0d(!EQҷ ! P6 T1Q|t(*oce$~ &`h 5H(?2p0;`ښcdñoRpO___ $KעItݳNĹ/FJ1@rX|C`w-=\nM3m!`8d83f桃8͞6-z8 r=A6 LEv8y:t8طo_wZZj*Ef[J6LI/cN8.M R;gVr_Td(utN}XdF"ee![Vfm%fJk4(!:ˆ#$E7!7Wu!לM*jppؼyزeشiļyĞ={ Hlݺu?Kgh#Vf͚\wuFgA2۷O@&fAZl9ECRYU pA: /TW}Q]^.֟7GK*[1߂PXOB6ydPuCj^ \r% jwܹ78UqF`fMʅfUX4djvFX({fyrTZy}svP/NnL#pizC#.h۶&FV>"+3`<e[î6%gyy }T諉./h*a^^7hƍl6j_lٲs+q>َe.̙3H+J.0L#0U@bXY>k37{Fj}5D6 Ul$Z>yĂMf, @8ak]e_`rYjzbSNi>577ŵ }ɍ&EsoTI%c¨QtFJ]or-p\fծ+W<FP]F EU~ѪcY(LB} tMƍ On+2#K,J6v ȷ,}Ɏ i}"$UDǏ2Y*@gj޽bhZgwp^10¤H}Bi-1}_3FiFo`FoX35"z"p;41ɤN1h"OTkJ @m`'e) ayum rv8쯊;o,. }K.[A 14^n%Kݔt#/?`re{ _Owq0#@(PMŅpٍHڱ}~АuwI(6+ܦvwMQNTmg6 g)Дz8"sڿY^~R^/EXpz :eAr)a<Wh_,(%LWV90#P`BvcNF%F7#k%~ C+=X{ow;\҄6=reiʗ8uT+o l0HܹSbsX)ڣ3R_y6,? Υ %Q80#PZlucHlX>g;g#/MM8tz㘱P*"hdzv{"΃ yuM_a }Ν: J6}Dbb;$ }mD80_F`ރ.m}!GFo M+ Vmi5 F`J0z5)$"ԺgHړ|dYɖ?Śp$dJqjIP_^\vq4n9#ZēbTl!y⭭yqwMm #Tm0Ĥ>yҨl64TamX`26M8WyK/4:_/90#SNXی~tbӞ)C1Cbɖ2 vR\D૭I] T1n ze+n u(A>Mב>ە/dSq>yn1\o:]?PGt=y& Qz !#n_yWۈiu7(`F00l4ubG]cU$3ee?XU;M7&]9]z"/ƉL EuxR8i-R\%dЗi6e2L`ӗhaj4777nܹbX#+VQUU\j aXh鈴iF|*`F(A Ԝʴ٫8,r*ԓ7&al`b8Y슭lm1 r +>}x{"o UW3?J'9ZȺ͗&''O0`+HVm5`FYKc#qfpgoPMzhe ⢔xfUx?EnBXo,dY%*5qKA~v>C Zj k\.EU"H4i[K8hTxl\O޽{(ҦUL#0@J6j4k-ǟDepjQܑ=\(&T$֩ b|XA~h)>tsY5&ukQa,VhE4J+fBϘ1C^(گ#90u؍OR˚6m\bBD+6n8M]򩐉`F(S&KJ˖ k6,[a]DvqQx]jǾ~JCVyTH> 2N[ QȏFY"{ ?\,^)S~'\^UCK\9kA:կRvEVݗ4P_Ӿ8`FJ6,wɮʵsPdM܂ K[AږDZGZ3pЫϡ0Ki265$h$1 C9cXfUUŁaXQckD?:+B'BkvoN}򭿿$)5DKl~~~\CPPP|xA" feeї^<;ۊ%0&aNA~pLXUF`J6&YEq㉏ wׯc5``&I |& ?(Xʫ$֘FYE,Š[wp|lٲыl6[& "h)Lq>^;ΔD$v$l㦥 7B\7ːEwIО={"_|EF$h M1@F@ D]d\A@DP=~ֿm-jH&_k(`Gdv@ BQG$麭5#ׄ?Nhko(I\> \ߠ6N6yį!|lBJAZ.1#9,& nlx<F`Ž@%]a̗ebKvOM@nr(zaJ6[%|b0`KAz$Oqbbda-5]#SY:BYx_={vwy6D= ~L &3ap]%D>@c/@hZn_y fQǍ'0B\q"==I  C!ցNoɱ8;ñ(K3MJ%Ѵb$?:|%$gPP)ðzKͣV!zx ˫/ݟGhaxھOj▷g %ݎq9GIC٩_ $F7lؠym+F~+9א̙K ar?#WtKvfdt5Eq$v1cxx$hi'#Эr?RVi{t.QNOC<9}&6PblXs a?%onwO$PoƄG:՛zA׭{(y{:)&e,}M21bĔ;Ū ,[\׺ukѧO }M[ѣ)5\?À)ുނP%!lfF 8'[;Eu@P"!4`jHqNol ~YoYOe:;vZ˖-M%-Z؎eO=v+Y״d})ҵk0A0`LlV-K+(WUpLʳd ]F`%Z&=Tڄ0Yo l6M+yv(98)ؠw9qڵsf͚mҤ(o͛~{ ZYk7gY0V.))i*%k7M<c\O @d0M" |M>5EF`#!PbV*\]EbC+phe=a]Mw#uuIK}B ~7|g$?m/'s٨Tw++CQF"99YSmۖGi+2^?rEPoQ:Iy-m*fbF` +ٴۣ\7Dž|V!e춢##4YUDJQ(t߽dɒ֯_tR{۴wgP*u (K $_}#~qUW9seV7mWi>`F#+6Dr] 6,Yf"9F`@~dɖ ]."q .P;Y뷍\(d0^ec̦FzR%Vn\s E"L#0@ t%*">Zs(WF>Zr~=|/iD"˺0#а%mF< 6xMQ,+ 1GŲMdW7׬\"in" GRISFiӦ}$<F`B6! Hp؀ 6BF`lZYWMry=wƱ c4lΓ8pC͢n۶Mp =3(0#6V:Gs}P*hf=SG䤉~D^'>RF+9Q㍱-Fܸ_ȍdQaBMj*~m1o<(ڍ(0#DL(*"р㚢Y$oztmR.6Ny睷ʈ#$ XN*B!#0@F6xx A0jOhty)gʈloXl]UodLh[/hD. _͛7LK`E&F`>qduEZ{>UY =!Zh82vy Uהǐ+hoMgϞ9΋>mİk^L(ڦke01#4<UEh>kXp/ߣ|,VSEG95*Bb0]沂A8S h/[>VmĿ31`W\! Z"m@bq0#а(4t(_5,(x `Qϲ.w-P?n'pǁB ^JjYX% mZWц]7:ׯNAn5lXx#4l, b7l(xT(2mteEP8Iazf>Ӫ0)i>2|,8p_yQFݻX/k(t zmkϟ?`6 K>Q5Rr /I&O>5(=pP~u +6&TcccaIx״| SmVf\ك۸qM/8G!8 KXɓ'˗ɳU}I)3\ CuL dXNB{#0 SyjAMhJ3.A LM6mxŸqDTT,neZrr.lIvvP雿``X^χWun@3ۨ\qwSj}2E 3f+uEn`چeZmaɉpD{ i6⸸C%P<75B!肾u>a֭6g~XE||HHH͛7zҙs1ʫE#~fU5Yg;C|e 5 A>F*yz U/UEU˭ mҵxpB0@}BE X4i*؍PeSqMXF %ںeH3~8Xr\9 g?Mrs6#jא`r/!`[ʙ8틲_V68tJjM٠PB6-M[!BNJ31#0#Peؒ]ePʣ,&F`F`‚O| uWFVgHMkG)S< WC>F`FM]F-Ev,iI1HŻl 3#0# !JGK(+"(w#@Ͷ꩎M ]Fim1#0#0A`)^o ;e]ocF`FQHވ)^ ]%z<8F`F`CP&kv3#0#4l:{v878s.Ξ eUOm6]AڎFaW`UP'"iNv_~VmʫkD/GxH MV٫\$v}bɆbUlc+UdBʶ:+5M} ^^m?p;޿O|,21U[<@NYV^վxߡٻNT甸cNNnVyмdxAi6;Dri;mVmۃ̨Oq:UXth=ѭjG|4!= @VRy<'tVk[kɉ%;]|lZc6x2iZw`-/ ] i~ݒ$*XvXF"P{+TJU:N1AF5HS4R2<{FI )H,[bʺZ$.ê-N ~VmLRxiՎƱFf?/Vq<)23bz!X,&pP!Y)6,1IZmXi6qF9+>)͕jZvϕx! El:wtM| C紪u6߫ڈ82K^Z4U{6tj ᢌ (cSK|љGԭ܅>ڝF"P{zEq)6q ?aA -QĐu6; ~p"2m+I@V7&Rb.|kQuٝb\,|\h&ECޙ-/_.z`XSH$@4oD+?XmeFJ?" !Ϋ$A&٩9e$|ŸvF-/oGn_g`o=IDAT[Aӹ0;ʛ+v72"PBrCG%PjeqKa9$eF'* '0~ v<+ӺoeJjn>v7/ pi ]QBܯ):=]KŵL}E)R}7o)" 6O`̣1ftjx!*q]&h-*_ţҝbA#qE;Ѕ_~uy>2V5ǙN=-[*ɵpMLoP5uwh珍tĢc䂦 Biy{(Vu\Db{U5g*/+v;qLsv@\睘3A_vʧ^xT%٧Uu~>1go4|6Wo쫭ю;@_| w261Zw^V4/:.s 2\sy55 +}n{#j=q 5A~K*2kgh"^b^sBgFUU)N9=4Mjq*~Z :v;9 l66=ߔ_!UE_Y{sy( G{x!USMN\[g! Idء|' VԞQUAcn6)ڢNSL?ݏ4\tWUm!~ʌs[yt9b^. 40qoFh`[lɶ@ˉ:usW +|G%{#ڰ(T/k3=8ʐ,8 odB2 ²&׾lG E)XP%S;o@+}ab+< >q ^U [VPtCg5{qJ,*{g)4)1њ˗ŹխwEK.\#-rIc4c~DV5X\z/euir?7W^.{es1i [jlƒCg'jK"<*OdE)C8|25 ڀ?7F͏^C kڷ t (D_g4keKKӵ0F`Im$LNx'՘YywY0DqѽH DoʂH>P^&:huT { 3NP؞=PD/g>j[i %\jCn^kdཧأqq$OWFcvc=0TʑT4x!ZzQN5}0+)v/q06.xtN3XvqPu0~z3,;ڭ'}_V򨋨{K;d6squVn'XDlv 2S}4Ekopi4ey{ jd8~ o+oYCXsCqG>~۴]xG>=2UWj\ 7?;D*C`參Q=9VW&F"Cj>*|#/q!'+Y OG]Ih5DUa+^JE;l(O^gL|j>ɖxfr| j*xM?P!f t~VEW<} &ziMKG^M?2.IAL+0( xyKHyś^o'n &-!| xL_,E.tF&xS4v"L0}NC7:0?E;IA϶IP ~u>^ |}D@Cw9|-zQF_#`j9dIFEᢛt LȺ5#s_#GRWm:gFƅ p߿1WB䷉tg2ۗw U)נQg2ہڹ(%~g/A:䒴 " B\|_-z3Ur^, ~f^_n5ʮ!'Ӧū?~pc0RPAs"d d,rJ26w[5>wSMA@U4(נE#+qBhOa%lŁ}a%&CiNA{Ysʭg# e>8:4jO+iͦL_H?W['^- fTԡg?UrviL([4ͣl3U@3OxS=7GDJ91出f,.;Dsz8>\g^pxqC&€Ix}ozT :)kZ&2{ܿ7>&h;y⇛,g7qm"`*&*"Ф{. ]EU-=^qL1`2[T&B:mA;#NSG$ wO.Bo}yeE%%'DQax<ҿ2"-~+8WD|0YO?yM1"Dn0)c!G1etP_4d^q .D )Mކ0*;!I/?!czxxgD;s)C'n 8C;vE}u`p@=y`>q xamӾ]Ts@J+kᣋAnr?qdo0)HظA/@+KxX'_磵's{aFV:߁wO. |ōf"I?%?砍g?L/E%t/#~pNG`.f7\hXGCst=|NFPw grppoW c N9zޭ~] miKw;sw}-T'byOuF{.o0 e9>?R~C MRh7O]6 uo9̖!fҡ3glY&(Fj`ïEFNgLF|hgeEWlE_gh_*nYͨeʪcmhG$|lU>rF'g]~ܶ᷇ ~Zl$=C4mM;(_cX&(vNh";ʦrvK7zsPHQA6EYl-:xגWA-plU"z~KtGH?"£ =|.qTJYBd/~,/BeׁF; \{\: &8 gG>"(GW) Kؾ9j隧BW0A S܋K". uA+Bibn4:WD;!OISV S>.5y":=U򏮵}i:_t ҥA5*ފ5j&M(&F `_B\|Goo;9of?OoءXMQ𡎔7]U:"`ՔH<(TќN;9g-݌5=|V\‡p(zmͯn阜{b2[X^卞n> mDQHxܞy#Rpp3?sS nvj]KRNiŔ/w7A24~@DP2 ,behDd>Po<*8,r"rh#q/ۺ M Y @K%UXW<쌪-ʜ7Vr1t&S4pZ5Y E;-nqvD_9[uW٣nn}o#}풵L8q8W@H)]Vi7vXOWHBҐ]כ> !We*KԤpi-;BuI&]u*˰4:WE'[uλq~'yT|1Ivί6`gϛK/Ei8?[JDJ>!Oz9w u*7{|qR\lg}et $ >Ű;h¦䗈zU$y`a$ nZh{FT@>u*cQ@S\@i/ [@kGGfSyoaaX?`~_>HHva;%a;~$8cbhE\cd |jxm}5pU[~ }ҵh%+5`XRa( ָ~NljgwϒT5Xao Xmߋo1;3i~kkn^:+Aa"$LuJD=p[2.E}up?p3QF⅒ M0 8gOӴ=2}V+tJ0AW:4kɾ~{wOO7ݪ>MO$=r;|ԷKTޢ1(Apcbj\7C6EvpțRf' t w'4=1 L%g:ᱻ L!8oދdhKv%vHyCY4~hUj\Gq@u}%f\!ڱgj`J I$#J+u,!><6{Ѿgm= לq6YM=qIr]Gfm2 >X旻 ݴ> 4/MEطZZۊ<#+ܿ2pkS~c{W=~յhn0scG׈ꎊ\0]RV!ݨ@\)P&`5-!?`e\rÐ39.:>kWn'0iTMe4uvkލ,x7*5 IM=IFSoԥu*p]<^` %qq6ͷԗG`]Y5D\_+ê_p-!Odǁօm D[#s*^nz4j_x]uݯP4B k t%;Gԁl$ e- 19;$ 6 k<<R{at!#P_ }9MLIM8ux78ɥ^F79Y} hDC;0)7+A%q7f xXJ 6\Z_jX Igo%%)S ǚV2GPnP!C 9%_Ŵ:ǗCc-,i "V! QFH|>Mܔ3h}u=,:54y,rdkLҔ)N^"ϵ=jh8?4^VV/4>OѴrz &Ec;~H |[[}]mW)Y],藞S0e텔i'˩))ms0mN/Ǐf[ ۶h2TGpeEzM#ipz\QPcC mr>`Iɦ/I)`X?^ <L LqeVmɪ]e*cZ@MůF;ҮoeR@T | t}i ک՗>WߵZnY):w&pVww å !r 0 DO.Ic6ј$Ą(*dcqW!A%d! r G!0_޼a]U]U_uzj鬍Uw9EܦEaز/ g?($oǚ_涇'Kiʪi٦0'Y]6qAǸʶQQ6uv">P?W"ԇ|Ξs,FVY،%0zώl:6+8#SޞuGgX٘lhO ][?k'4/Ӯ{LqJ 6g TP}i*Oy/↟HgNg< Oq:2EOٰt2w9FsԩhYy/ 7TWfv: ]nTq1E'muFRD[K/ S,ǂb&7t,,sE> WLH_wVK ܱ-(J l G1`!]-wuYfrz@Rö 8 +y6 qk\Hܴ{/Rj7UwSrMhMa57?!v&)f66U`=kch7j> dqvt@^P=?Y;e Xry]|8֕cw<`c]+XXSz$b0CWxSK*bɓC6bi󒶸>MeQ71eįi^;O[޽u`c_r_+cܓiET`}Dgf*u,|aS&|^c|z9]KǶuAbG'23Ǐ=]^;"Vdr(ohuOgcsh~͸@rF4~?ٶ㓇/㚿B?+6oΊ#3>';Ij=G.Fwٳ4M瘲js3ذg='̽j~27]>Vb|ҙb9|pgּcAж_Iͯ;%ۢ X.J$߃q)(Sܴۢ5+MJv,5ꞴтgzmVC H8rkGn$Y=6H~7vl,|r2ײ.G,l26=̆gѧt\抾Qώ =Pq;JC6nOo<' 8[V>hrcүf0 8skl)v4= ɫ/aGjMw8/wl9Eߢ&Yj߂N/[b.>̻g,拏STbWPE{lF׿[Jx/0= % g5js_7KLӨ߸x ,m捻~<5 g uBѴ׃8D#Il#ʄ#q7s鐊K6-[9:0Gj l,3=od'uo>>{w\߯4ЛB}dŸ>ځxGg4Ğ9fqqAc:`8K퉯0{f4m75~~eB;<R3xVm+;]AW$bqvO!^NSTwo i14٘ro벇wK΂u)ަ4{cd_\yvuKHclytԥJKFޱdEHY"6 BF w!7#L^2{ɤӑKi¼T oʻۑh_^VVrq.#l+܍_)^#~lc9­ۏ|{S5rOu2hagd-ɗ&VC"`ZuZXiר\II  px܄:{94CΊ!lcF1w:5cVH[~EX6TY<קl3%w)Wz~dM pA [dhqs(Ėtf]ZS*Axrݿ!_Ad ]/3e9~Q_Ic}~"Y>fI RW>mBL!?#WPE;c6c3 f7K,!cbn\fzmgF m"ٹ-F2M71G&nm"rvKʉN37ȯtѣ,ߴK;sZ#mc r3'~q쟇"3@mq"fӠ|HD@N\~1f͢ bfxVԏhW&OG3|H+{;vaqkÎ n+ŠڔiuDNDp!B^4>9r>r|98 ,4C#Ʈ8ݹȎ'muT_~O?kfn;:ig~KyY y,כ09/ٽHC hl/$w{/PSD@N wB{is"a;tmwſ _ vH@iH܍6#/u'/|0?*§ d#2q\+_[7} hVQΣgV68h;xaƪdXY޼NUhɹUȓMyj~smu% /cҧEUs/ipKIwBnE@6(6EfW>3ND@O>q(G}w Kw˵!d3_iqKKԣvِ6[ND@D@D@D@O|/" lۖU&KUwlY]ND@D@D@D@NUlf eo(40çvd}U4J&" " " "ph"U+jǮS>aÒÏ3f+hq+n45oUU&g7`ͶYMD@D@D@D@D(Z\f>!MeEg#_D@D@D@D@ ! J6ܺAEl$ˉ@ZC.͔A7)["J:z9s4mžY)mZAvV0]xRRCD@D@D@D"JvW m Y6caϬE@D@D@D@?-d;SFeo6isdgVX3+m@/F3,^XkӺ;sFɼZfY̢4ȉ@aZޒ77aў]X",2O)؍g#D@D@D@D@h5KvPȢ wcf CCיֳsPtkY|hu%?o~q^}ݱDQN,Ĕ[$>vqfI2VdNvE@D@D@D@DڄC 7j_ 9|Wc:v-p튀@jiX`7V%^=p7V;&x9O8GNho ˌRɹ:SS<ZD@D@D@D@Dl!W#/ UHSkyX^e-v*HD@D@D@Dhs|WW7=9÷e@V!+Z—pi@؊lC @as-bʶ@^@v;u괛U-"Y CE@D@D@D@' mϙ30udnBg-" " " "Dqn$ds9{d`ocD@D@D@D@D@@iV!i;Tg*ض}}RhJ I+ض=s̃9RC힖@=RN?g!hh +$K=zÇ3uٷ{Z " " " " M+Hݷoߊ={q[DꡩhvO،d)ؽ{>q\{9=1:t >$d)#Gڹsgu`hA`5\IUVV*o0SClžCH-nJ\uN͓D@D@D@D@D]xg_rG,X\|K& 6cȫH-uvu׹ NND@D@D@D@-SmS#>h$,l7]pa>z+VND@D@D@D@ 8S1 ,B*]رcݗ^z)rmaO 8VND@D@D@D@v^E:7M8v/b_K~)킢NRD@D@D@D@2leQ 1cYf;vK~2Ц+8c*sg̘Ξ=]zu]u튞NVD@D@D@D /PhE"':t% pL L0!X4V3P(X '" " " "~w%;}BU: )ˑCXɟL" " " " 홀ldǐtR -r" " " " "pL 0 { return mv } } return 65536 } // CryptoMaxPlaintextSizeVal returns the maximum allowed plaintext size in // bytes. // It is calculated as CryptoMaxCiphertextSizeVal minus 16 bytes, which accounts // for the authentication tag overhead used in authenticated encryption schemes // such as AES-GCM. func CryptoMaxPlaintextSizeVal() int { return CryptoMaxCiphertextSizeVal() - 16 } spike-sdk-go-0.16.4/config/env/database.go000066400000000000000000000102451511163700700202320ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strconv" "time" ) // DatabaseJournalModeVal returns the SQLite journal mode to use. // It can be configured using the SPIKE_NEXUS_DB_JOURNAL_MODE environment // variable. // // If the environment variable is not set, it defaults to "WAL" // (Write-Ahead Logging). func DatabaseJournalModeVal() string { s := os.Getenv(NexusDBJournalMode) if s != "" { return s } return "WAL" } // DatabaseBusyTimeoutMsVal returns the SQLite busy timeout in milliseconds. // It can be configured using the SPIKE_NEXUS_DB_BUSY_TIMEOUT_MS environment // variable. The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 5000 milliseconds (5 seconds). func DatabaseBusyTimeoutMsVal() int { p := os.Getenv(NexusDBBusyTimeoutMS) if p != "" { bt, err := strconv.Atoi(p) if err == nil && bt > 0 { return bt } } return 5000 } // DatabaseMaxOpenConnsVal returns the maximum number of open database // connections. It can be configured using the SPIKE_NEXUS_DB_MAX_OPEN_CONNS // environment variable. The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 10 connections. func DatabaseMaxOpenConnsVal() int { p := os.Getenv(NexusDBMaxOpenConns) if p != "" { moc, err := strconv.Atoi(p) if err == nil && moc > 0 { return moc } } return 10 } // DatabaseMaxIdleConnsVal returns the maximum number of idle database // connections. It can be configured using the SPIKE_NEXUS_DB_MAX_IDLE_CONNS // environment variable. The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 5 connections. func DatabaseMaxIdleConnsVal() int { p := os.Getenv(NexusDBMaxIdleConns) if p != "" { mic, err := strconv.Atoi(p) if err == nil && mic > 0 { return mic } } return 5 } // DatabaseConnMaxLifetimeSecVal returns the maximum lifetime duration for a // database connection. It can be configured using the // SPIKE_NEXUS_DB_CONN_MAX_LIFETIME environment variable. // The value should be a valid Go duration string (e.g., "1h", "30m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 1 hour. func DatabaseConnMaxLifetimeSecVal() time.Duration { p := os.Getenv(NexusDBConnMaxLifetime) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return time.Hour } // DatabaseOperationTimeoutVal returns the duration to use for database // operations. It can be configured using the SPIKE_NEXUS_DB_OPERATION_TIMEOUT // environment variable. The value should be a valid Go duration string // (e.g., "10s", "1m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 15 seconds. func DatabaseOperationTimeoutVal() time.Duration { p := os.Getenv(NexusDBOperationTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 15 * time.Second } // DatabaseInitializationTimeoutVal returns the duration to wait for database // initialization. // // The timeout is read from the environment variable // `SPIKE_NEXUS_DB_INITIALIZATION_TIMEOUT`. If this variable is set and its // value can be parsed as a duration (e.g., "1m30s"), it is used. // Otherwise, the function defaults to a timeout of 30 seconds. func DatabaseInitializationTimeoutVal() time.Duration { p := os.Getenv(NexusDBInitializationTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 30 * time.Second } // DatabaseSkipSchemaCreationVal determines if schema creation should be // skipped. It checks the "SPIKE_NEXUS_DB_SKIP_SCHEMA_CREATION" env variable // to decide. // If the env variable is set and its value is "true", it returns true. // Otherwise, it returns false. func DatabaseSkipSchemaCreationVal() bool { p := os.Getenv(NexusDBSkipSchemaCreation) if p != "" { s, err := strconv.ParseBool(p) if err == nil { return s } } return false } spike-sdk-go-0.16.4/config/env/doc.go000066400000000000000000000017471511163700700172420ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package env provides environment variable configuration for SPIKE components. // It defines constants for all SPIKE environment variables and provides utility // functions to read and parse these variables with appropriate defaults. // // The package covers configuration for: // - SPIKE Nexus (API URLs, backend storage, database settings, TLS ports) // - SPIKE Keeper (peers, update intervals, TLS ports) // - SPIKE Pilot (recovery directories, memory warnings) // - Trust roots (bootstrap, keeper, nexus, pilot, lite workload) // - Shamir secret sharing (shares, threshold) // - Recovery and validation settings // - Logging and system-level configuration // // All configuration values can be customized via environment variables with // sensible defaults provided when variables are not set. package env spike-sdk-go-0.16.4/config/env/env.go000066400000000000000000000061301511163700700172540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env // Sort alphabetically. const BannerEnabled = "SPIKE_BANNER_ENABLED" const BootstrapConfigMapName = "SPIKE_BOOTSTRAP_CONFIGMAP_NAME" const BootstrapForce = "SPIKE_BOOTSTRAP_FORCE" const HTTPClientDialerKeepAlive = "SPIKE_HTTP_CLIENT_DIALER_KEEP_ALIVE" const HTTPClientDialerTimeout = "SPIKE_HTTP_CLIENT_DIALER_TIMEOUT" const HTTPClientExpectContinueTimeout = "SPIKE_HTTP_CLIENT_EXPECT_CONTINUE_TIMEOUT" const HTTPClientIdleConnTimeout = "SPIKE_HTTP_CLIENT_IDLE_CONN_TIMEOUT" const HTTPClientMaxConnsPerHost = "SPIKE_HTTP_CLIENT_MAX_CONNS_PER_HOST" const HTTPClientMaxIdleConns = "SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS" const HTTPClientMaxIdleConnsPerHost = "SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST" const HTTPClientResponseHeaderTimeout = "SPIKE_HTTP_CLIENT_RESPONSE_HEADER_TIMEOUT" const HTTPClientTimeout = "SPIKE_HTTP_CLIENT_TIMEOUT" const HTTPClientTLSHandshakeTimeout = "SPIKE_HTTP_CLIENT_TLS_HANDSHAKE_TIMEOUT" const HTTPServerReadHeaderTimeout = "SPIKE_HTTP_SERVER_READ_HEADER_TIMEOUT" const KeeperTLSPort = "SPIKE_KEEPER_TLS_PORT" const NexusAPIURL = "SPIKE_NEXUS_API_URL" const NexusBackendStore = "SPIKE_NEXUS_BACKEND_STORE" const NexusCryptoMaxCiphertextSize = "SPIKE_NEXUS_CRYPTO_MAX_CIPHERTEXT_SIZE" const NexusDBBusyTimeoutMS = "SPIKE_NEXUS_DB_BUSY_TIMEOUT_MS" const NexusDBConnMaxLifetime = "SPIKE_NEXUS_DB_CONN_MAX_LIFETIME" const NexusDBInitializationTimeout = "SPIKE_NEXUS_DB_INITIALIZATION_TIMEOUT" const NexusDBJournalMode = "SPIKE_NEXUS_DB_JOURNAL_MODE" const NexusDBMaxIdleConns = "SPIKE_NEXUS_DB_MAX_IDLE_CONNS" const NexusDBMaxOpenConns = "SPIKE_NEXUS_DB_MAX_OPEN_CONNS" const NexusDBOperationTimeout = "SPIKE_NEXUS_DB_OPERATION_TIMEOUT" const NexusDBSkipSchemaCreation = "SPIKE_NEXUS_DB_SKIP_SCHEMA_CREATION" const NexusDataDir = "SPIKE_NEXUS_DATA_DIR" const NexusKeeperPeers = "SPIKE_NEXUS_KEEPER_PEERS" const NexusKeeperUpdateInterval = "SPIKE_NEXUS_KEEPER_UPDATE_INTERVAL" const NexusMaxEntryVersions = "SPIKE_NEXUS_MAX_SECRET_VERSIONS" const NexusPBKDF2IterationCount = "SPIKE_NEXUS_PBKDF2_ITERATION_COUNT" const NexusRecoveryMaxInterval = "SPIKE_NEXUS_RECOVERY_MAX_INTERVAL" const NexusShamirMaxShareCount = "SPIKE_NEXUS_SHAMIR_MAX_SHARE_COUNT" const NexusShamirShares = "SPIKE_NEXUS_SHAMIR_SHARES" const NexusShamirThreshold = "SPIKE_NEXUS_SHAMIR_THRESHOLD" const NexusTLSPort = "SPIKE_NEXUS_TLS_PORT" const PilotRecoveryDir = "SPIKE_PILOT_RECOVERY_DIR" const PilotShowMemoryWarning = "SPIKE_PILOT_SHOW_MEMORY_WARNING" const SPIFFEEndpointSocket = "SPIFFE_ENDPOINT_SOCKET" const SPIFFESourceTimeout = "SPIKE_SPIFFE_SOURCE_TIMEOUT" const StackTracesOnLogFatal = "SPIKE_STACK_TRACES_ON_LOG_FATAL" const SystemLogLevel = "SPIKE_SYSTEM_LOG_LEVEL" const TrustRoot = "SPIKE_TRUST_ROOT" const TrustRootBootstrap = "SPIKE_TRUST_ROOT_BOOTSTRAP" const TrustRootKeeper = "SPIKE_TRUST_ROOT_KEEPER" const TrustRootLiteWorkload = "SPIKE_TRUST_ROOT_LITE_WORKLOAD" const TrustRootNexus = "SPIKE_TRUST_ROOT_NEXUS" const TrustRootPilot = "SPIKE_TRUST_ROOT_PILOT" spike-sdk-go-0.16.4/config/env/http.go000066400000000000000000000147251511163700700174540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strconv" "time" ) // HTTPClientDialerKeepAliveVal returns the keep-alive duration for the HTTP // client's dialer. It can be configured using the // SPIKE_HTTP_CLIENT_DIALER_KEEP_ALIVE environment variable. // The value should be a valid Go duration string (e.g., "30s", "1m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 30 seconds. func HTTPClientDialerKeepAliveVal() time.Duration { p := os.Getenv(HTTPClientDialerKeepAlive) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 30 * time.Second } // HTTPClientDialerTimeoutVal returns the timeout duration for the HTTP // client's dialer. It can be configured using the // SPIKE_HTTP_CLIENT_DIALER_TIMEOUT environment variable. // The value should be a valid Go duration string (e.g., "30s", "1m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 30 seconds. func HTTPClientDialerTimeoutVal() time.Duration { p := os.Getenv(HTTPClientDialerTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 30 * time.Second } // HTTPClientExpectContinueTimeoutVal returns the timeout for Expect: 100-continue // responses from the server. It can be configured using the // SPIKE_HTTP_CLIENT_EXPECT_CONTINUE_TIMEOUT environment variable. // The value should be a valid Go duration string (e.g., "5s", "10s"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 5 seconds. func HTTPClientExpectContinueTimeoutVal() time.Duration { p := os.Getenv(HTTPClientExpectContinueTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 5 * time.Second } // HTTPClientIdleConnTimeoutVal returns the maximum duration an idle connection // will remain idle before closing. It can be configured using the // SPIKE_HTTP_CLIENT_IDLE_CONN_TIMEOUT environment variable. // The value should be a valid Go duration string (e.g., "30s", "1m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 30 seconds. func HTTPClientIdleConnTimeoutVal() time.Duration { p := os.Getenv(HTTPClientIdleConnTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 30 * time.Second } // HTTPClientMaxConnsPerHostVal returns the maximum number of connections // per host. It can be configured using the SPIKE_HTTP_CLIENT_MAX_CONNS_PER_HOST // environment variable. The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 10 connections. func HTTPClientMaxConnsPerHostVal() int { p := os.Getenv(HTTPClientMaxConnsPerHost) if p != "" { moc, err := strconv.Atoi(p) if err == nil && moc > 0 { return moc } } return 10 } // HTTPClientMaxIdleConnsVal returns the maximum number of idle connections // across all hosts. It can be configured using the // SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS environment variable. // The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 100 connections. func HTTPClientMaxIdleConnsVal() int { p := os.Getenv(HTTPClientMaxIdleConns) if p != "" { mic, err := strconv.Atoi(p) if err == nil && mic > 0 { return mic } } return 100 } // HTTPClientMaxIdleConnsPerHostVal returns the maximum number of idle // connections per host. It can be configured using the // SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST environment variable. // The value must be a positive integer. // // If the environment variable is not set or contains an invalid value, // it defaults to 10 connections. func HTTPClientMaxIdleConnsPerHostVal() int { p := os.Getenv(HTTPClientMaxIdleConnsPerHost) if p != "" { mic, err := strconv.Atoi(p) if err == nil && mic > 0 { return mic } } return 10 } // HTTPClientResponseHeaderTimeoutVal returns the timeout for waiting for a // server's response headers. It can be configured using the // SPIKE_HTTP_CLIENT_RESPONSE_HEADER_TIMEOUT environment variable. // The value should be a valid Go duration string (e.g., "10s", "30s"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 10 seconds. func HTTPClientResponseHeaderTimeoutVal() time.Duration { p := os.Getenv(HTTPClientResponseHeaderTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 10 * time.Second } // HTTPClientTimeoutVal returns the overall timeout for HTTP client requests. // It can be configured using the SPIKE_HTTP_CLIENT_TIMEOUT environment variable. // The value should be a valid Go duration string (e.g., "60s", "2m"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 60 seconds. func HTTPClientTimeoutVal() time.Duration { p := os.Getenv(HTTPClientTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 60 * time.Second } // HTTPClientTLSHandshakeTimeoutVal returns the timeout for the TLS handshake. // It can be configured using the SPIKE_HTTP_CLIENT_TLS_HANDSHAKE_TIMEOUT // environment variable. The value should be a valid Go duration string // (e.g., "10s", "30s"). // // If the environment variable is not set or contains an invalid duration, // it defaults to 10 seconds. func HTTPClientTLSHandshakeTimeoutVal() time.Duration { p := os.Getenv(HTTPClientTLSHandshakeTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 10 * time.Second } // HTTPServerReadHeaderTimeoutVal returns the timeout for reading HTTP request // headers on the server side. It can be configured using the // SPIKE_HTTP_SERVER_READ_HEADER_TIMEOUT environment variable. The value should // be a valid Go duration string (e.g., "10s", "30s"). // // This timeout helps prevent slowloris attacks by limiting how long the server // will wait for request headers to be sent by the client. // // If the environment variable is not set or contains an invalid duration, // it defaults to 10 seconds. func HTTPServerReadHeaderTimeoutVal() time.Duration { p := os.Getenv(HTTPServerReadHeaderTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 10 * time.Second } spike-sdk-go-0.16.4/config/env/keeper.go000066400000000000000000000037171511163700700177470ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strconv" "strings" "github.com/spiffe/spike-sdk-go/log" ) // KeepersVal retrieves and parses the keeper peer configurations from the // environment. It reads SPIKE_NEXUS_KEEPER_PEERS environment variable which // should contain a comma-separated list of keeper URLs. // // The environment variable should be formatted as: // 'https://localhost:8443,https://localhost:8543,https://localhost:8643' // // The SPIKE Keeper address mappings will be automatically assigned starting // with the key "1" and incrementing by 1 for each subsequent SPIKE Keeper. // // Returns: // - map[string]string: Mapping of keeper IDs to their URLs // // Panics if: // - SPIKE_NEXUS_KEEPER_PEERS is not set func KeepersVal() map[string]string { const fName = "KeepersVal" p := os.Getenv(NexusKeeperPeers) if p == "" { log.FatalLn( fName, "message", "SPIKE_NEXUS_KEEPER_PEERS must be configured in the environment", ) } urls := strings.Split(p, ",") // Check for duplicate and empty URLs urlMap := make(map[string]bool) for i, u := range urls { trimmedURL := strings.TrimSpace(u) if trimmedURL == "" { log.FatalLn( fName, "message", "empty url found", "position", i+1, ) } // Validate URL format and security if !validURL(trimmedURL) { log.FatalLn( fName, "message", "invalid url format", "position", i+1, ) } if urlMap[trimmedURL] { log.FatalLn( fName, "message", "duplicate url found", "position", i+1, ) } urlMap[trimmedURL] = true } // The key of the map is the Shamir Shard index (starting from 1), and // the value is the Keeper URL that corresponds to that shard index. peers := make(map[string]string) for i, u := range urls { peers[strconv.Itoa(i+1)] = strings.TrimSpace(u) } return peers } spike-sdk-go-0.16.4/config/env/net.go000066400000000000000000000017041511163700700172540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import "os" // NexusTLSPortVal returns the TLS port for the Spike Nexus service. // It reads from the SPIKE_NEXUS_TLS_PORT environment variable. // If the environment variable is not set, it returns the default port ":8553". func NexusTLSPortVal() string { p := os.Getenv(NexusTLSPort) if p != "" { return p } return ":8553" } // KeeperTLSPortVal returns the TLS port for the Spike Keeper service. // It first checks for a port specified in the SPIKE_KEEPER_TLS_PORT // environment variable. // If no environment variable is set, it defaults to ":8443". // // The returned string is in the format ":port" suitable for use with // net/http Listen functions. func KeeperTLSPortVal() string { p := os.Getenv(KeeperTLSPort) if p != "" { return p } return ":8443" } spike-sdk-go-0.16.4/config/env/out.go000066400000000000000000000051221511163700700172730ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strings" ) // BannerEnabledVal returns whether to show the initial banner on app start // based on the SPIKE_BANNER_ENABLED environment variable. // // The function reads the SPIKE_BANNER_ENABLED environment variable and returns: // - true if the variable is not set (default behavior) // - true if the variable is set to "true" (case-insensitive) // - false for any other value // // The environment variable value is trimmed of whitespace and converted to // lowercase before comparison. func BannerEnabledVal() bool { s := os.Getenv(BannerEnabled) s = strings.ToLower(strings.TrimSpace(s)) if s == "" { return true } return s == "true" } // ShowMemoryWarningVal returns whether to display a warning when the system // cannot lock memory based on the SPIKE_PILOT_SHOW_MEMORY_WARNING environment // variable. // // The function reads the SPIKE_PILOT_SHOW_MEMORY_WARNING environment variable // and returns: // - false if the variable is not set (default behavior) // - true if the variable is set to "true" (case-insensitive) // - false for any other value // // The environment variable value is trimmed of whitespace and converted to // lowercase before comparison. // // This warning is typically shown when memory locking fails, which could // impact security-sensitive operations that require pages to remain in RAM. func ShowMemoryWarningVal() bool { s := os.Getenv(PilotShowMemoryWarning) s = strings.ToLower(strings.TrimSpace(s)) if s == "" { return false } return s == "true" } // StackTracesOnLogFatalVal returns whether to print stack traces when // `log.FatalLn` is called, based on the SPIKE_STACK_TRACES_ON_LOG_FATAL // environment variable. // // The function reads the SPIKE_STACK_TRACES_ON_LOG_FATAL environment variable // and returns: // - false if the variable is not set (default behavior - clean exit) // - true if the variable is set to "true" (case-insensitive - panic with stack trace) // - false for any other value // // The environment variable value is trimmed of whitespace and converted to // lowercase before comparison. // // By default, log.FatalLn performs a clean exit to avoid leaking sensitive // information in production stack traces. Enable this for development and // testing purposes. func StackTracesOnLogFatalVal() bool { s := os.Getenv(StackTracesOnLogFatal) s = strings.ToLower(strings.TrimSpace(s)) if s == "" { return false } return s == "true" } spike-sdk-go-0.16.4/config/env/recover.go000066400000000000000000000025141511163700700201330ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "time" ) // RecoveryOperationMaxIntervalVal returns the maximum interval duration for // recovery backoff retry algorithm. The interval is determined by the // environment variable `SPIKE_NEXUS_RECOVERY_MAX_INTERVAL`. // // If the environment variable is not set or is not a valid duration // string, then it defaults to 60 seconds. func RecoveryOperationMaxIntervalVal() time.Duration { e := os.Getenv(NexusRecoveryMaxInterval) if e != "" { if d, err := time.ParseDuration(e); err == nil { return d } } return 60 * time.Second } // RecoveryKeeperUpdateIntervalVal returns the duration between keeper updates // for SPIKE Nexus. It first attempts to read the duration from the // SPIKE_NEXUS_KEEPER_UPDATE_INTERVAL environment variable. If the environment // variable is set and contains a valid duration string (as parsed by // time.ParseDuration), that duration is returned. Otherwise, it returns a // default value of 5 minutes. func RecoveryKeeperUpdateIntervalVal() time.Duration { e := os.Getenv(NexusKeeperUpdateInterval) if e != "" { if d, err := time.ParseDuration(e); err == nil { return d } } return 5 * time.Minute } spike-sdk-go-0.16.4/config/env/secret.go000066400000000000000000000013641511163700700177550ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strconv" ) // MaxSecretVersionsVal returns the maximum number of versions to retain // for each secret. It reads from the SPIKE_NEXUS_MAX_SECRET_VERSIONS // environment variable which should contain a positive integer value. // If the environment variable is not set, contains an invalid integer, or // specifies a non-positive value, it returns the default of 10 versions. func MaxSecretVersionsVal() int { p := os.Getenv(NexusMaxEntryVersions) if p != "" { mv, err := strconv.Atoi(p) if err == nil && mv > 0 { return mv } } return 10 } spike-sdk-go-0.16.4/config/env/shamir.go000066400000000000000000000046221511163700700177530ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strconv" ) // ShamirSharesVal returns the total number of shares to be used in Shamir's // Secret Sharing. It reads the value from the SPIKE_NEXUS_SHAMIR_SHARES // environment variable. // // Returns: // - The number of shares specified in the environment variable if it's a // valid positive integer // - The default value of 3 if the environment variable is unset, empty, // or invalid // // This determines the total number of shares that will be created when // // splitting a secret. func ShamirSharesVal() int { p := os.Getenv(NexusShamirShares) if p != "" { mv, err := strconv.Atoi(p) if err == nil && mv > 0 { return mv } } return 3 } // ShamirMaxShareCountVal returns the maximum allowed number of shares in // Shamir's Secret Sharing scheme. It reads the value from the // SPIKE_NEXUS_SHAMIR_MAX_SHARE_COUNT environment variable. // // Returns: // - The maximum share count specified in the environment variable if it's // a valid positive integer // - The default value of 1000 if the environment variable is unset, empty, // or invalid // // This limit prevents excessive resource consumption when creating shares. // This variable also limits the maximum number of SPIKE Keeper instances that // a SPIKE deployment can support. func ShamirMaxShareCountVal() int { p := os.Getenv(NexusShamirMaxShareCount) if p != "" { mv, err := strconv.Atoi(p) if err == nil && mv > 0 { return mv } } return 1000 } // ShamirThresholdVal returns the minimum number of shares required to // reconstruct the secret in Shamir's Secret Sharing scheme. // It reads the value from the SPIKE_NEXUS_SHAMIR_THRESHOLD environment // variable. // // Returns: // - The threshold specified in the environment variable if it's a valid // positive integer // - The default value of 2 if the environment variable is unset, empty, // or invalid // // This threshold value determines how many shares are needed to recover the // original secret. It should be less than or equal to the total number of // shares (ShamirShares()). func ShamirThresholdVal() int { p := os.Getenv(NexusShamirThreshold) if p != "" { mv, err := strconv.Atoi(p) if err == nil && mv > 0 { return mv } } return 2 } spike-sdk-go-0.16.4/config/env/spiffe.go000066400000000000000000000017431511163700700177450ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "time" ) // SPIFFESourceTimeoutVal returns the timeout duration for creating a SPIFFE // X509Source and fetching the initial SVID from the SPIFFE Workload API. // It can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT environment // variable. The value should be a valid Go duration string (e.g., "30s", "1m"). // // This timeout prevents indefinite blocking if there are issues with the // SPIFFE Workload API socket (e.g., agent not running, socket permissions, // network issues). // // If the environment variable is not set or contains an invalid duration, // it defaults to 30 seconds. func SPIFFESourceTimeoutVal() time.Duration { p := os.Getenv(SPIFFESourceTimeout) if p != "" { d, err := time.ParseDuration(p) if err == nil { return d } } return 30 * time.Second } spike-sdk-go-0.16.4/config/env/trust.go000066400000000000000000000056061511163700700176540ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import ( "os" "strings" ) const defaultTrustRoot = "spike.ist" // TrustRootFromEnv retrieves the trust root from an environment variable. // It takes the name of an environment variable and returns its value. // The environment variable name must start with "SPIKE_TRUST_ROOT" for // security. If the environment variable name doesn't follow this pattern, // is not set, or is empty, it returns the default trust root "spike.ist". // // Parameters: // - trustRootEnvVar: The name of the environment variable to read // (must start with "SPIKE_TRUST_ROOT") // // Returns: // - The value of the environment variable, or "spike.ist" if not set or // invalid name func TrustRootFromEnv(trustRootEnvVar string) string { // Validate that the environment variable follows the expected pattern. // If the pattern does not match, return the default trust root. if !strings.HasPrefix(trustRootEnvVar, TrustRoot) { return defaultTrustRoot } tr := os.Getenv(trustRootEnvVar) if tr == "" { return defaultTrustRoot } return tr } // TrustRootVal returns the default trust root from the SPIKE_TRUST_ROOT // environment variable. This is a convenience function that calls // TrustRootFromEnv with the default trust root environment variable name. // // Returns: // - The value of SPIKE_TRUST_ROOT environment variable, or "spike.ist" // if not set func TrustRootVal() string { return TrustRootFromEnv(TrustRoot) } // TrustRootForKeeperVal returns the trust root for SPIKE Keeper from the // SPIKE_TRUST_ROOT_KEEPER environment variable. This is a convenience function // that calls TrustRootFromEnv with the Keeper-specific environment variable. // // Returns: // - The value of SPIKE_TRUST_ROOT_KEEPER environment variable, or "spike.ist" // if not set func TrustRootForKeeperVal() string { return TrustRootFromEnv(TrustRootKeeper) } // TrustRootForPilotVal returns the trust root for SPIKE Pilot from the // SPIKE_TRUST_ROOT_PILOT environment variable. This is a convenience function // that calls TrustRootFromEnv with the Pilot-specific environment variable. // // Returns: // - The value of SPIKE_TRUST_ROOT_PILOT environment variable, or "spike.ist" // if not set func TrustRootForPilotVal() string { return TrustRootFromEnv(TrustRootPilot) } // TrustRootForLiteWorkloadVal returns the trust root for SPIKE Lite Workloads // from the SPIKE_TRUST_ROOT_LITE_WORKLOAD environment variable. This is a // convenience function that calls TrustRootFromEnv with the Lite // Workload-specific environment variable. // // Returns: // - The value of SPIKE_TRUST_ROOT_LITE_WORKLOAD environment variable, or // "spike.ist" if not set func TrustRootForLiteWorkloadVal() string { return TrustRootFromEnv(TrustRootLiteWorkload) } spike-sdk-go-0.16.4/config/env/url.go000066400000000000000000000013331511163700700172660ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import "os" const nexusDefaultAPIRoot = "https://localhost:8553" // NexusAPIRootVal retrieves the SPIKE Nexus API root URL from the environment. // It reads the value from the SPIKE_NEXUS_API_URL environment variable. // // Returns: // - The Nexus API root URL from the environment variable if set // - The default value of "https://localhost:8553" if the environment // variable is unset or empty func NexusAPIRootVal() string { apiRoot := os.Getenv(NexusAPIURL) if apiRoot == "" { return nexusDefaultAPIRoot } return apiRoot } spike-sdk-go-0.16.4/config/env/validate.go000066400000000000000000000006471511163700700202640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package env import "net/url" // validURL validates that a URL is properly formatted and uses HTTPS func validURL(urlStr string) bool { pu, err := url.Parse(urlStr) if err != nil { return false } return pu.Scheme == "https" && pu.Host != "" } spike-sdk-go-0.16.4/coverage.html000066400000000000000000000532441511163700700165710ustar00rootroot00000000000000 mock: Go Coverage Report
not tracked not covered covered
spike-sdk-go-0.16.4/crypto/000077500000000000000000000000001511163700700154205ustar00rootroot00000000000000spike-sdk-go-0.16.4/crypto/algo.go000066400000000000000000000026261511163700700166770ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package crypto import ( "crypto/rand" "encoding/hex" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) var reader = rand.Read // AES256KeySize defines the size of a key in bytes for AES-256 encryption. const AES256KeySize = 32 // AES256Seed generates a cryptographically secure random 256-bit key suitable // for use with AES-256 encryption. The key is returned as a hexadecimal-encoded // string. // // Returns: // - string: A 64-character hexadecimal string representing the 256-bit key, // empty string on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrCryptoFailedToCreateCipher: if random key generation fails // // The function uses a cryptographically secure random number generator to // ensure the generated key is suitable for cryptographic use. The resulting hex // string can be decoded back to bytes using hex.DecodeString when needed for // encryption. func AES256Seed() (string, *sdkErrors.SDKError) { // Generate a 256-bit key key := make([]byte, AES256KeySize) _, err := reader(key) if err != nil { failErr := sdkErrors.ErrCryptoFailedToCreateCipher.Wrap(err) failErr.Msg = "failed to generate random key" return "", failErr } return hex.EncodeToString(key), nil } spike-sdk-go-0.16.4/crypto/crypto_test.go000066400000000000000000000310651511163700700203330ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package crypto import ( "encoding/hex" "errors" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestAES256Seed_Success tests successful AES-256 seed generation func TestAES256Seed_Success(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() // Create deterministic reader for testing reader = func(b []byte) (int, error) { // Fill with deterministic data for i := range b { b[i] = byte(i) } return len(b), nil } seed, err := AES256Seed() assert.Nil(t, err) assert.NotEmpty(t, seed) assert.Equal(t, AES256KeySize*2, len(seed)) // Hex encoding doubles the length // Verify it's valid hex decoded, decodeErr := hex.DecodeString(seed) assert.NoError(t, decodeErr) assert.Equal(t, AES256KeySize, len(decoded)) } // TestAES256Seed_Error tests AES256Seed when random generation fails func TestAES256Seed_Error(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() // Mock reader that returns an error reader = func(_ []byte) (int, error) { return 0, errors.New("mock random generation failure") } seed, err := AES256Seed() assert.Empty(t, seed) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrCryptoFailedToCreateCipher)) } // TestAES256Seed_Uniqueness tests that multiple calls generate different seeds func TestAES256Seed_Uniqueness(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() // Use a counter to generate different values each time counter := 0 reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(counter + i) } counter++ return len(b), nil } seed1, err1 := AES256Seed() seed2, err2 := AES256Seed() assert.Nil(t, err1) assert.Nil(t, err2) assert.NotEqual(t, seed1, seed2, "Seeds should be unique") } // TestRandomString_Success tests successful random string generation func TestRandomString_Success(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() // Use deterministic reader reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(i % 62) // Keep within alphanumeric range } return len(b), nil } tests := []struct { name string length int }{ {"Length1", 1}, {"Length8", 8}, {"Length26", 26}, {"Length64", 64}, {"Length100", 100}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := RandomString(tt.length) assert.Equal(t, tt.length, len(result)) // Verify all characters are alphanumeric for _, c := range result { assert.True(t, (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'), "Character %c should be alphanumeric", c) } }) } } // TestRandomString_EmptyLength tests RandomString with zero length func TestRandomString_EmptyLength(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() reader = func(b []byte) (int, error) { return len(b), nil } result := RandomString(0) assert.Equal(t, "", result) } // TestRandomString_CharacterDistribution tests that all character classes are used func TestRandomString_CharacterDistribution(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() // Create a reader that returns different values to cover all character ranges position := 0 reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(position % 256) position++ } return len(b), nil } result := RandomString(100) // Check that we have at least some variety in the output assert.True(t, len(result) == 100) // Verify it only contains valid characters for _, c := range result { assert.True(t, strings.ContainsRune(letters, c), "Character %c should be from the letters set", c) } } // TestToken_Format tests that Token generates correct format func TestToken_Format(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(i % 62) } return len(b), nil } token := Token() // Verify format: "spike." + 26 characters assert.True(t, strings.HasPrefix(token, "spike.")) assert.Equal(t, 6+26, len(token)) // "spike." is 6 chars + 26 random chars // Extract and verify the random part randomPart := token[6:] assert.Equal(t, 26, len(randomPart)) // Verify all characters in random part are alphanumeric for _, c := range randomPart { assert.True(t, (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'), "Character %c should be alphanumeric", c) } } // TestToken_Uniqueness tests that multiple Token calls generate different tokens func TestToken_Uniqueness(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() counter := 0 reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(counter + i) } counter++ return len(b), nil } token1 := Token() token2 := Token() assert.NotEqual(t, token1, token2, "Tokens should be unique") assert.True(t, strings.HasPrefix(token1, "spike.")) assert.True(t, strings.HasPrefix(token2, "spike.")) } // TestID_Format tests that ID generates correct length func TestID_Format(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(i % 62) } return len(b), nil } id := ID() assert.Equal(t, 8, len(id)) // Verify all characters are alphanumeric for _, c := range id { assert.True(t, (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'), "Character %c should be alphanumeric", c) } } // TestID_Uniqueness tests that multiple ID calls generate different IDs func TestID_Uniqueness(t *testing.T) { // Save original reader and restore after test originalReader := reader defer func() { reader = originalReader }() counter := 0 reader = func(b []byte) (int, error) { for i := range b { b[i] = byte(counter + i) } counter++ return len(b), nil } id1 := ID() id2 := ID() id3 := ID() assert.Equal(t, 8, len(id1)) assert.Equal(t, 8, len(id2)) assert.Equal(t, 8, len(id3)) assert.NotEqual(t, id1, id2, "IDs should be unique") assert.NotEqual(t, id2, id3, "IDs should be unique") assert.NotEqual(t, id1, id3, "IDs should be unique") } // TestDeterministicReader_Read tests the Read method func TestDeterministicReader_Read(t *testing.T) { seed := []byte("test seed") reader := NewDeterministicReader(seed) buffer := make([]byte, 16) n, err := reader.Read(buffer) assert.NoError(t, err) assert.Equal(t, 16, n) assert.NotEmpty(t, buffer) } // TestDeterministicReader_Consistency tests that same seed produces same output func TestDeterministicReader_Consistency(t *testing.T) { seed := []byte("test seed") // Create two readers with same seed reader1 := NewDeterministicReader(seed) reader2 := NewDeterministicReader(seed) buffer1 := make([]byte, 32) buffer2 := make([]byte, 32) n1, err1 := reader1.Read(buffer1) n2, err2 := reader2.Read(buffer2) assert.NoError(t, err1) assert.NoError(t, err2) assert.Equal(t, 32, n1) assert.Equal(t, 32, n2) assert.Equal(t, buffer1, buffer2, "Same seed should produce same output") } // TestDeterministicReader_DifferentSeeds tests that different seeds produce different output func TestDeterministicReader_DifferentSeeds(t *testing.T) { seed1 := []byte("seed one") seed2 := []byte("seed two") reader1 := NewDeterministicReader(seed1) reader2 := NewDeterministicReader(seed2) buffer1 := make([]byte, 32) buffer2 := make([]byte, 32) _, err1 := reader1.Read(buffer1) _, err2 := reader2.Read(buffer2) require.NoError(t, err1) require.NoError(t, err2) assert.NotEqual(t, buffer1, buffer2, "Different seeds should produce different output") } // TestDeterministicReader_MultipleReads tests multiple consecutive reads func TestDeterministicReader_MultipleReads(t *testing.T) { seed := []byte("test seed") reader := NewDeterministicReader(seed) // Read in chunks buffer1 := make([]byte, 16) buffer2 := make([]byte, 16) buffer3 := make([]byte, 16) n1, err1 := reader.Read(buffer1) n2, err2 := reader.Read(buffer2) n3, err3 := reader.Read(buffer3) assert.NoError(t, err1) assert.NoError(t, err2) assert.NoError(t, err3) assert.Equal(t, 16, n1) assert.Equal(t, 16, n2) assert.Equal(t, 16, n3) // Buffers should be different (sequential reads) assert.NotEqual(t, buffer1, buffer2) assert.NotEqual(t, buffer2, buffer3) } // TestDeterministicReader_LargeRead tests reading more data than initial buffer func TestDeterministicReader_LargeRead(t *testing.T) { seed := []byte("test seed") reader := NewDeterministicReader(seed) // Read more than the initial SHA-256 hash size (32 bytes) // Note: io.Reader may return less than requested buffer := make([]byte, 100) totalRead := 0 // Read in multiple calls to fill the buffer for totalRead < 100 { n, err := reader.Read(buffer[totalRead:]) assert.NoError(t, err) assert.True(t, n > 0, "Should read at least some data") totalRead += n } assert.Equal(t, 100, totalRead) assert.NotEmpty(t, buffer) } // TestDeterministicReader_EmptyBuffer tests reading into empty buffer func TestDeterministicReader_EmptyBuffer(t *testing.T) { seed := []byte("test seed") reader := NewDeterministicReader(seed) buffer := make([]byte, 0) n, err := reader.Read(buffer) assert.NoError(t, err) assert.Equal(t, 0, n) } // TestDeterministicReader_SmallReads tests many small consecutive reads func TestDeterministicReader_SmallReads(t *testing.T) { seed := []byte("test seed") reader := NewDeterministicReader(seed) // Collect data from many small reads var collected []byte for i := 0; i < 100; i++ { buffer := make([]byte, 1) n, err := reader.Read(buffer) assert.NoError(t, err) assert.Equal(t, 1, n) collected = append(collected, buffer[0]) } assert.Equal(t, 100, len(collected)) } // TestDeterministicReader_ReproducibleStream tests that stream is reproducible func TestDeterministicReader_ReproducibleStream(t *testing.T) { seed := []byte("reproducible seed") // First stream reader1 := NewDeterministicReader(seed) stream1 := make([]byte, 200) _, err1 := reader1.Read(stream1) // Second stream with same seed reader2 := NewDeterministicReader(seed) stream2 := make([]byte, 200) _, err2 := reader2.Read(stream2) require.NoError(t, err1) require.NoError(t, err2) assert.Equal(t, stream1, stream2, "Stream should be reproducible with same seed") } // TestAES256KeySize_Constant tests the AES256KeySize constant func TestAES256KeySize_Constant(t *testing.T) { assert.Equal(t, 32, AES256KeySize, "AES-256 key size should be 32 bytes") } // TestLettersConstant tests the letters constant has expected characters func TestLettersConstant(t *testing.T) { // Verify letters contains lowercase for c := 'a'; c <= 'z'; c++ { assert.True(t, strings.ContainsRune(letters, c), "letters should contain lowercase letter %c", c) } // Verify letters contains uppercase for c := 'A'; c <= 'Z'; c++ { assert.True(t, strings.ContainsRune(letters, c), "letters should contain uppercase letter %c", c) } // Verify letters contains digits for c := '0'; c <= '9'; c++ { assert.True(t, strings.ContainsRune(letters, c), "letters should contain digit %c", c) } // Verify total length assert.Equal(t, 62, len(letters), "letters should have 62 characters (26+26+10)") } // TestNewDeterministicReader_NilSeed tests creating reader with nil seed func TestNewDeterministicReader_NilSeed(t *testing.T) { reader := NewDeterministicReader(nil) require.NotNil(t, reader) buffer := make([]byte, 32) n, err := reader.Read(buffer) assert.NoError(t, err) assert.Equal(t, 32, n) assert.NotEmpty(t, buffer) } // TestNewDeterministicReader_EmptySeed tests creating reader with empty seed func TestNewDeterministicReader_EmptySeed(t *testing.T) { reader := NewDeterministicReader([]byte{}) require.NotNil(t, reader) buffer := make([]byte, 32) n, err := reader.Read(buffer) assert.NoError(t, err) assert.Equal(t, 32, n) assert.NotEmpty(t, buffer) } spike-sdk-go-0.16.4/crypto/doc.go000066400000000000000000000014401511163700700165130ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package crypto provides cryptographic utilities for SPIKE. // // It includes functionality for: // - Generating cryptographically secure random strings and identifiers // - Creating AES-256 encryption keys // - Character class-based random string generation with support for // predefined classes (\w, \d, \x) and custom ranges (e.g., A-Za-z0-9) // - Deterministic readers for testing and reproducible random data // - Template-based string generation // // All random generation uses cryptographically secure random number generators // to ensure suitability for security-sensitive operations. package crypto spike-sdk-go-0.16.4/crypto/id.go000066400000000000000000000062441511163700700163510ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package crypto import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) const letters = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // RandomString generates a cryptographically secure random string of the // specified length using alphanumeric characters (a-z, A-Z, 0-9). // // Security Note: This function will fatally crash the process // (via log.FatalErr) if the system's cryptographic random number generator // fails. This is an intentional security decision, not a bug. Here's why: // // 1. CSPRNG failure indicates a critical system-level security compromise // 2. Continuing with potentially weak/predictable random values would create // security vulnerabilities (weak tokens, predictable IDs, compromised // secrets) // 3. There is no safe fallback - using non-cryptographic randomness or // deterministic values would be catastrophically insecure // 4. This is consistent with other security-critical failures in the codebase // (SVID acquisition, Shamir operations, Pilot restrictions) // 5. Failing loudly prevents silent security degradation and forces operators // to address the underlying system issue // // The crypto/rand documentation states that Read failures are extremely rare // and indicate serious OS-level problems. When this happens, the entire // system's security is compromised, not just this function. // // Parameters: // - n: length of the random string to generate // // Returns: // - string: the generated random alphanumeric string func RandomString(n int) string { const fName = "RandomString" bytes := make([]byte, n) if _, err := reader(bytes); err != nil { failErr := sdkErrors.ErrCryptoRandomGenerationFailed.Wrap(err) failErr.Msg = "cryptographic random number generator failed" log.FatalErr(fName, *failErr) } for i, b := range bytes { bytes[i] = letters[b%byte(len(letters))] } return string(bytes) } // Token generates a cryptographically secure random token with the "spike." // prefix. The token consists of the prefix followed by 26 random alphanumeric // characters, resulting in a format like "spike.AbCd1234EfGh5678IjKl9012Mn". // // Security Note: This function will fatally crash the process if the // cryptographic random number generator fails. See RandomString() documentation // for the security rationale behind this behavior. // // Returns: // - string: the generated token in the format "spike.<26-char-random-string>" func Token() string { id := RandomString(26) return "spike." + id } // ID generates a cryptographically secure random identifier consisting of // 8 alphanumeric characters. Suitable for use as short, unique identifiers. // // Security Note: This function will fatally crash the process if the // cryptographic random number generator fails. See RandomString() documentation // for the security rationale behind this behavior. // // Returns: // - string: the generated 8-character random alphanumeric string func ID() string { return RandomString(8) } spike-sdk-go-0.16.4/crypto/io.go000066400000000000000000000035761511163700700163710ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package crypto import ( "crypto/sha256" ) // DeterministicReader implements io.Reader to generate deterministic // pseudo-random data based on a seed. It uses SHA-256 hashing to create a // repeatable stream of bytes. type DeterministicReader struct { data []byte pos int } // Read implements io.Reader interface. It returns deterministic data by reading // from the internal buffer and generating new data using SHA-256 when needed. // // If the current position reaches the end of the data buffer, it generates // a new block by hashing the current data. This ensures a continuous, // deterministic stream of data. // // This implementation properly satisfies the io.Reader interface contract. // The error return is always nil since deterministic hashing operations cannot // fail, but is required for io.Reader interface compliance. // // Parameters: // - p []byte: Buffer to read data into // // Returns: // - n int: Number of bytes read // - err error: Always nil (deterministic reads never fail) func (r *DeterministicReader) Read(p []byte) (n int, err error) { if r.pos >= len(r.data) { // Generate more deterministic data if needed hash := sha256.Sum256(r.data) r.data = hash[:] r.pos = 0 } n = copy(p, r.data[r.pos:]) r.pos += n return n, nil } // NewDeterministicReader creates a new DeterministicReader initialized with // the SHA-256 hash of the provided seed data. // // Parameters: // - seed []byte: Initial seed data to generate the deterministic stream // // Returns: // - *DeterministicReader: New reader instance initialized with the seed func NewDeterministicReader(seed []byte) *DeterministicReader { hash := sha256.Sum256(seed) return &DeterministicReader{ data: hash[:], pos: 0, } } spike-sdk-go-0.16.4/errors/000077500000000000000000000000001511163700700154145ustar00rootroot00000000000000spike-sdk-go-0.16.4/errors/doc.go000066400000000000000000000010001511163700700164770ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package errors provides sentinel error values used throughout the SPIKE API. // These errors represent common failure conditions such as authentication // failures, validation errors, initialization issues, and communication // problems. They enable consistent error handling and identification across // SPIKE services. package errors spike-sdk-go-0.16.4/errors/errors.go000066400000000000000000000030751511163700700172640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package errors // FromCode maps an ErrorCode to its corresponding SDKError using the // automatically populated error registry. This is used to convert error codes // received from API responses back to proper SDKError instances. // // The registry is automatically populated when errors are defined using the // register() function, ensuring new errors are immediately available without // manual updates to this function. // // If the error code is not recognized, it returns ErrGeneralFailure. // // Parameters: // - code: the error code to map // // Returns: // - *SDKError: the corresponding SDK error instance func FromCode(code ErrorCode) *SDKError { // Defensive coding: While concurrent reads to a map are safe, unless a // write happens concurrently; if we enable dynamic error registration // later down the line, without a mutex the behavior of this code will be // undeterministic. errorRegistryMu.RLock() err, ok := errorRegistry[code] errorRegistryMu.RUnlock() if ok { return err } return ErrGeneralFailure } // MaybeError converts an error to its string representation if the error is // not nil. If the error is nil, it returns an empty string. // // Parameters: // - err: the error to convert to a string // // Returns: // - string: the error message if err is non-nil, empty string otherwise func MaybeError(err error) string { if err != nil { return err.Error() } return "" } spike-sdk-go-0.16.4/errors/registry.go000066400000000000000000000023441511163700700176160ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package errors import "sync" type ErrorCode string // errorRegistry maps ErrorCodes to their corresponding SDKError instances. // This map is automatically populated when errors are defined using the // register() function, ensuring FromCode() always has up-to-date mappings. // Access is protected by errorRegistryMu for thread safety. var ( errorRegistry = make(map[ErrorCode]*SDKError) errorRegistryMu sync.RWMutex ) // register creates a new SDKError, adds it to the global registry, and // returns it. This ensures that all defined errors are automatically available // in FromCode(). This function is thread-safe. // // Parameters: // - code: The error code string // - msg: The human-readable error message // - wrapped: Optional wrapped error (typically nil for predefined errors) // // Returns: // - *SDKError: The newly created and registered error func register(code string, msg string, wrapped error) *SDKError { err := New(ErrorCode(code), msg, wrapped) errorRegistryMu.Lock() errorRegistry[err.Code] = err errorRegistryMu.Unlock() return err } spike-sdk-go-0.16.4/errors/sdk.go000066400000000000000000000110411511163700700165210ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package errors import ( "errors" "fmt" ) // SDKError represents a structured error in the SPIKE SDK. It provides error // codes for programmatic handling, human-readable messages, and support for // error wrapping to maintain error chains. // // Usage patterns (see ADR-0028): // 1. All SDK and non-CLI SPIKE errors shall use SDKError // 2. All comparisons shall be done with errors.Is() // 3. Context information shall be included in the Msg field // 4. Import SDK errors as `sdkErrors` consistently for easier code search // 5. Use predefined errors and wrap them with context, don't create from codes // // Example: // // // Use predefined errors // return sdkErrors.ErrEntityNotFound // // // Or wrap with additional context // return sdkErrors.ErrEntityNotFound.Wrap(dbErr) // // // Check error types // if errors.Is(err, sdkErrors.ErrEntityNotFound) { // // Handle not found error // } type SDKError struct { // Code is the error code for programmatic error handling Code ErrorCode // Msg is the human-readable error message Msg string // Wrapped is the underlying error, if any Wrapped error } // New creates a new SDKError with the specified error code, message, and // optional wrapped error. // // Note: In most cases, you should use predefined errors // (e.g., ErrEntityNotFound) and wrap them with .Wrap() instead of creating new // errors from codes directly. // // Parameters: // - code: the error code identifying the error type // - msg: human-readable error message providing context // - wrapped: optional underlying error to wrap (can be nil) // // Returns: // - *SDKError: a new SDK error instance // // Example: // // // Creating a custom error (rare, prefer using predefined errors) // err := sdkErrors.New( // sdkErrors.ErrEntityNotFound.Code, // "secret 'prod-api-key' not found in vault 'production'", // dbErr, // ) func New(code ErrorCode, msg string, wrapped error) *SDKError { return &SDKError{ Code: code, Msg: msg, Wrapped: wrapped, } } // Error implements the error interface, returning a formatted error message // that includes the error code, message, and recursively includes wrapped // error messages. // // Returns: // - string: formatted error message with error code and full error chain func (e *SDKError) Error() string { if e.Wrapped != nil { return fmt.Sprintf("[%s] %s: %v", e.Code, e.Msg, e.Wrapped) } return fmt.Sprintf("[%s] %s", e.Code, e.Msg) } // Unwrap returns the wrapped error, enabling error chain traversal with // errors.Is() and errors.As() from the standard library. // // Returns: // - error: the wrapped error, or nil if no error was wrapped func (e *SDKError) Unwrap() error { return e.Wrapped } // Wrap creates a new SDKError that wraps the provided error, preserving // the current error's code and message while adding the new error to the // error chain. // // Parameters: // - err: the error to wrap in the error chain // // Returns: // - *SDKError: a new SDK error with the same code and message but with // the provided error wrapped // // Example: // // // Wrap a database error with entity not found error // return sdkErrors.ErrEntityNotFound.Wrap(dbErr) func (e *SDKError) Wrap(err error) *SDKError { return &SDKError{ Code: e.Code, Msg: e.Msg, Wrapped: err, } } // Is enables error comparison by error code using errors.Is() from the // standard library. Two SDKErrors are considered equal if they have the // same error code. // // Parameters: // - target: the error to compare against // // Returns: // - bool: true if target is an SDKError with the same error code // // Example: // // if errors.Is(err, sdkErrors.ErrEntityNotFound) { // // Handle not found error // } func (e *SDKError) Is(target error) bool { var t *SDKError if errors.As(target, &t) { return e.Code == t.Code } return false } // Clone creates a shallow copy of the SDKError. This is useful when you need // to modify the Msg field of a sentinel error without mutating the original. // // Returns: // - *SDKError: a new SDK error with the same code, message, and wrapped // error as the original // // Example: // // // Copy a sentinel error to customize the message // failErr := sdkErrors.ErrEntityNotFound.Copy() // failErr.Msg = "secret 'prod-api-key' not found" // return failErr func (e *SDKError) Clone() *SDKError { return &SDKError{ Code: e.Code, Msg: e.Msg, Wrapped: e.Wrapped, } } spike-sdk-go-0.16.4/errors/sentinel.go000066400000000000000000000455021511163700700175720ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package errors // // General error codes // // ErrGeneralFailure indicates a general unspecified failure. var ErrGeneralFailure = register("gen_general_failure", "general failure", nil) // ErrNilContext indicates a nil context was provided. var ErrNilContext = register("gen_nil_context", "nil context", nil) // // Cluster operations // // ErrK8sReconciliationFailed indicates Kubernetes reconciliation failed. var ErrK8sReconciliationFailed = register("k8s_reconciliation_failed", "reconciliation failed", nil) // ErrK8sClientFailed indicates Kubernetes client creation failed. var ErrK8sClientFailed = register("k8s_client_failed", "failed to create Kubernetes client", nil) // ErrK8sResourceLookupFailed indicates Kubernetes resource lookup failed. var ErrK8sResourceLookupFailed = register("k8s_lookup_failed", "failed to lookup Kubernetes resource", nil) // // API/HTTP operations // // ErrAPIBadRequest indicates the API request was malformed or invalid. var ErrAPIBadRequest = register("api_bad_request", "bad request", nil) // ErrAPIEmptyPayload indicates the API request payload was empty. var ErrAPIEmptyPayload = register("api_empty_payload", "empty payload", nil) // ErrAPIFound indicates the requested resource was found. var ErrAPIFound = register("api_found", "found", nil) // ErrAPIInternal indicates an internal server error occurred. var ErrAPIInternal = register("api_internal_error", "internal error", nil) // ErrAPINotFound indicates the requested resource was not found. var ErrAPINotFound = register("api_not_found", "not found", nil) // ErrAPIPostFailed indicates the HTTP POST request failed. var ErrAPIPostFailed = register("api_post_failed", "post failed", nil) // ErrAPIResponseCodeInvalid indicates an invalid API response code was received. var ErrAPIResponseCodeInvalid = register("api_response_code_invalid", "invalid API response code", nil) // ErrAPIServerFault indicates a server-side fault occurred. var ErrAPIServerFault = register("api_server_fault", "server fault", nil) // // Entity operations // // ErrEntityCreationFailed indicates object creation failed. var ErrEntityCreationFailed = register("object_creation_failed", "creation failed", nil) // ErrEntityDeleted indicates the entity is marked as deleted. var ErrEntityDeleted = register("entity_deleted", "entity marked as deleted", nil) // ErrEntityDeletionFailed indicates object deletion failed. var ErrEntityDeletionFailed = register("object_deletion_failed", "deletion failed", nil) // ErrEntityExists indicates the entity already exists. var ErrEntityExists = register("entity_exists", "entity already exists", nil) // ErrEntityInvalid indicates the entity data is invalid. var ErrEntityInvalid = register("entity_invalid", "entity is invalid", nil) // ErrEntityLoadFailed indicates the entity failed to load from storage. var ErrEntityLoadFailed = register("entity_load_failed", "failed to load entity", nil) // ErrEntityNotFound indicates the requested entity was not found. var ErrEntityNotFound = register("entity_not_found", "entity not found", nil) // ErrEntityQueryFailed indicates the entity query operation failed. var ErrEntityQueryFailed = register("entity_query_failed", "failed to query entities", nil) // ErrEntitySaveFailed indicates the entity failed to get saved to storage. var ErrEntitySaveFailed = register("entity_save_failed", "failed to save entity", nil) // ErrEntityVersionInvalid indicates an invalid entity version was specified. var ErrEntityVersionInvalid = register("entity_version_invalid", "invalid version", nil) // ErrEntityVersionNotFound indicates the requested entity version was not found. var ErrEntityVersionNotFound = register("entity_version_not_found", "version not found", nil) // ErrEntityUndeletionFailed indicates object undeletion (restore) failed. var ErrEntityUndeletionFailed = register("object_undeletion_failed", "undeletion failed", nil) // // State operations // // ErrStateAlreadyInitialized indicates the system state is already initialized. var ErrStateAlreadyInitialized = register("state_already_initialized", "already initialized", nil) // ErrStateInitializationFailed indicates system state initialization failed. var ErrStateInitializationFailed = register("state_initialization_failed", "initialization failed", nil) // ErrStateNotAlive indicates the system state is not alive. var ErrStateNotAlive = register("state_not_alive", "not alive", nil) // ErrStateNotReady indicates the system state is not ready. var ErrStateNotReady = register("state_not_ready", "not ready", nil) // ErrStateIntegrityCheck indicates the system state integrity check failed. var ErrStateIntegrityCheck = register("state_integrity_check", "state integrity check failed", nil) // // Policy/RBAC/ABAC // // ErrAccessInvalidPermission indicates an invalid permission was specified. var ErrAccessInvalidPermission = register("access_invalid_permission", "invalid permission", nil) // ErrAccessUnauthorized indicates the requester lacks authorization for the operation. var ErrAccessUnauthorized = register("access_unauthorized", "unauthorized", nil) // // Root key management // // ErrRootKeyEmpty indicates the root key is empty. var ErrRootKeyEmpty = register("root_key_empty", "root key empty", nil) // ErrRootKeyMissing indicates the root key is missing. var ErrRootKeyMissing = register("root_key_missing", "root key missing", nil) // ErrRootKeyNotEmpty indicates the root key is not empty when expected to be. var ErrRootKeyNotEmpty = register("root_key_not_empty", "root key not empty", nil) // ErrRootKeySkipCreationForInMemoryMode indicates root key creation was skipped for in-memory mode. var ErrRootKeySkipCreationForInMemoryMode = register("root_key_skip_creation_for_in_memory_mode", "root key skip creation for in memory mode", nil) // ErrRootKeyUpdateSkippedKeyEmpty indicates the root key update was skipped because the key is empty. var ErrRootKeyUpdateSkippedKeyEmpty = register("root_key_update_skipped_key_empty", "root key update skipped key empty", nil) // // Shamir-related // // ErrShamirDuplicateIndex indicates a duplicate shard index was provided. var ErrShamirDuplicateIndex = register("shamir_duplicate_index", "shamir duplicate index", nil) // ErrShamirEmptyShard indicates a Shamir shard is empty. var ErrShamirEmptyShard = register("shamir_empty_shard", "shamir empty shard", nil) // ErrShamirInvalidIndex indicates an invalid Shamir shard index. var ErrShamirInvalidIndex = register("shamir_invalid_index", "shamir invalid index", nil) // ErrShamirNilShard indicates a nil Shamir shard was provided. var ErrShamirNilShard = register("shamir_nil_shard", "shamir nil shard", nil) // ErrShamirNotEnoughShards indicates insufficient Shamir shards for reconstruction. var ErrShamirNotEnoughShards = register("shamir_not_enough_shards", "shamir not enough shards", nil) // ErrShamirReconstructionFailed indicates Shamir secret reconstruction failed. var ErrShamirReconstructionFailed = register("shamir_reconstruction_failed", "shamir reconstruction failed", nil) // // Crypto operations // // ErrCryptoCipherNotAvailable indicates the requested cipher is not available. var ErrCryptoCipherNotAvailable = register("crypto_cipher_not_available", "cipher not available", nil) // ErrCryptoCipherVerificationFailed indicates cipher verification failed. var ErrCryptoCipherVerificationFailed = register("crypto_cipher_verification_failed", "cipher verification failed", nil) // ErrCryptoDecryptionFailed indicates data decryption failed. var ErrCryptoDecryptionFailed = register("crypto_decryption_failed", "decryption failed", nil) // ErrCryptoEncryptionFailed indicates data encryption failed. var ErrCryptoEncryptionFailed = register("crypto_encryption_failed", "encryption failed", nil) // ErrCryptoFailedToCreateCipher indicates cipher creation failed. var ErrCryptoFailedToCreateCipher = register("crypto_failed_to_create_cipher", "failed to create cipher", nil) // ErrCryptoFailedToCreateGCM indicates GCM mode creation failed. var ErrCryptoFailedToCreateGCM = register("crypto_failed_to_create_gcm", "failed to create GCM", nil) // ErrCryptoFailedToReadNonce indicates nonce reading failed. var ErrCryptoFailedToReadNonce = register("crypto_failed_to_read_nonce", "failed to read nonce", nil) // ErrCryptoFailedToReadVersion indicates version reading failed. var ErrCryptoFailedToReadVersion = register("crypto_failed_to_read_version", "failed to read version", nil) // ErrCryptoInvalidEncryptionKeyLength indicates the encryption key length is invalid. var ErrCryptoInvalidEncryptionKeyLength = register("crypto_invalid_encryption_key_length", "invalid encryption key length", nil) // ErrCryptoLowEntropy indicates insufficient entropy for cryptographic operations. var ErrCryptoLowEntropy = register("crypto_low_entropy", "low entropy", nil) // ErrCryptoNonceGenerationFailed indicates nonce generation failed. var ErrCryptoNonceGenerationFailed = register("crypto_nonce_generation_failed", "nonce generation failed", nil) // ErrCryptoNonceSizeMismatch indicates the nonce size does not match the cipher block size. var ErrCryptoNonceSizeMismatch = register("crypto_nonce_size_mismatch", "nonce size mismatch", nil) // ErrCryptoRandomGenerationFailed indicates random data generation failed. var ErrCryptoRandomGenerationFailed = register("crypto_random_generation_failed", "random generation failed", nil) // ErrCryptoUnsupportedCipherVersion indicates the requested cipher version is not supported. var ErrCryptoUnsupportedCipherVersion = register("crypto_unsupported_version", "unsupported crypto version", nil) // // Backing store infrastructure (internal store operations) // // ErrStoreCloseFailed indicates the backing store close operation failed. var ErrStoreCloseFailed = register("store_close_failed", "backing store close failed", nil) // ErrStoreInvalidConfiguration indicates the backing store configuration is invalid. var ErrStoreInvalidConfiguration = register("store_invalid_configuration", "invalid store configuration", nil) // ErrStoreInvalidEncryptionKey indicates the store encryption key is invalid. var ErrStoreInvalidEncryptionKey = register("store_invalid_encryption_key", "invalid store encryption key", nil) // // Filesystem operations // // ErrFSDirectoryCreationFailed indicates filesystem directory creation failed. var ErrFSDirectoryCreationFailed = register("fs_directory_creation_failed", "directory creation failed", nil) // ErrFSDirectoryDoesNotExist indicates the directory does not exist. var ErrFSDirectoryDoesNotExist = register("fs_directory_does_not_exist", "directory does not exist", nil) // ErrFSFailedToCheckDirectory indicates failed to check directory status. var ErrFSFailedToCheckDirectory = register("fs_failed_to_check_directory", "failed to check directory", nil) // ErrFSFailedToCreateDirectory indicates failed to create the directory. var ErrFSFailedToCreateDirectory = register("fs_failed_to_create_directory", "failed to create directory", nil) // ErrFSFailedToResolvePath indicates failed to resolve the filesystem path. var ErrFSFailedToResolvePath = register("fs_failed_to_resolve_path", "failed to resolve filesystem path", nil) // ErrFSFileCloseFailed indicates file close operation failed. var ErrFSFileCloseFailed = register("fs_file_close_failed", "file close failed", nil) // ErrFSFileIsNotADirectory indicates the path is not a directory. var ErrFSFileIsNotADirectory = register("fs_file_is_not_a_directory", "file is not a directory", nil) // ErrFSFileOpenFailed indicates file open operation failed. var ErrFSFileOpenFailed = register("fs_file_open_failed", "file open failed", nil) // ErrFSInvalidDirectory indicates an invalid directory path. var ErrFSInvalidDirectory = register("fs_invalid_directory", "invalid directory", nil) // ErrFSParentDirectoryDoesNotExist indicates the parent directory does not exist. var ErrFSParentDirectoryDoesNotExist = register("fs_parent_directory_does_not_exist", "parent directory does not exist", nil) // ErrFSPathCannotBeEmpty indicates the filesystem path cannot be empty. var ErrFSPathCannotBeEmpty = register("fs_path_cannot_be_empty", "filesystem path cannot be empty", nil) // ErrFSPathRestricted indicates the filesystem path is restricted for security reasons. var ErrFSPathRestricted = register("fs_path_restricted", "filesystem path is restricted for security reasons", nil) // ErrFSStreamCloseFailed indicates stream close operation failed. var ErrFSStreamCloseFailed = register("fs_stream_close_failed", "stream close failed", nil) // ErrFSStreamReadFailed indicates stream read operation failed. var ErrFSStreamReadFailed = register("fs_stream_read_failed", "stream read failed", nil) // ErrFSStreamWriteFailed indicates a stream write operation failed. var ErrFSStreamWriteFailed = register("stream_write_failed", "stream write failed", nil) // ErrFSStreamOpenFailed indicates stream open operation failed. var ErrFSStreamOpenFailed = register("fs_stream_open_failed", "stream open failed", nil) // // Data Processing // // ErrDataInvalidInput indicates invalid input data was provided. var ErrDataInvalidInput = register("data_invalid_input", "invalid input", nil) // ErrDataMarshalFailure indicates data marshaling failed. var ErrDataMarshalFailure = register("data_marshal_failure", "failed to marshal response body", nil) // ErrDataParseFailure indicates data parsing failed. var ErrDataParseFailure = register("data_parse_failure", "failed to parse request body", nil) // ErrDataReadFailure indicates data reading failed. var ErrDataReadFailure = register("data_read_failure", "failed to read request body", nil) // ErrDataUnmarshalFailure indicates data unmarshaling failed. var ErrDataUnmarshalFailure = register("data_unmarshal_failure", "failed to unmarshal request body", nil) // // String/Template operations // // ErrStringEmptyCharacterClass indicates an empty character class was specified. var ErrStringEmptyCharacterClass = register("string_empty_character_class", "empty character class", nil) // ErrStringEmptyCharacterSet indicates the character class resulted in an empty set. var ErrStringEmptyCharacterSet = register("string_empty_character_set", "character class resulted in empty set", nil) // ErrStringInvalidLength indicates an invalid length specification. var ErrStringInvalidLength = register("string_invalid_length", "invalid length specification", nil) // ErrStringInvalidRange indicates an invalid character range specification. var ErrStringInvalidRange = register("string_invalid_range", "invalid character range", nil) // ErrStringNegativeLength indicates the length cannot be negative. var ErrStringNegativeLength = register("string_negative_length", "length cannot be negative", nil) // // Network/Peer // // ErrNetPeerConnection indicates a problem connecting to a network peer. var ErrNetPeerConnection = register("net_peer_connection", "problem connecting to peer", nil) // ErrNetReadingRequestBody indicates a problem reading the request body. var ErrNetReadingRequestBody = register("net_reading_request_body", "problem reading request body", nil) // ErrNetReadingResponseBody indicates a problem reading the response body. var ErrNetReadingResponseBody = register("net_reading_response_body", "problem reading response body", nil) // ErrNetURLJoinPathFailed indicates URL path joining failed. var ErrNetURLJoinPathFailed = register("net_url_join_path_failed", "failed to join URL path", nil) // // Transaction operations // // ErrTransactionBeginFailed indicates `transaction begin` failed. var ErrTransactionBeginFailed = register("transaction_begin_failed", "failed to begin transaction", nil) // ErrTransactionCommitFailed indicates `transaction commit` failed. var ErrTransactionCommitFailed = register("transaction_commit_failed", "failed to commit transaction", nil) // ErrTransactionFailed indicates the transaction failed. var ErrTransactionFailed = register("transaction_failed", "transaction failed", nil) // ErrTransactionRollbackFailed indicates transaction rollback failed. var ErrTransactionRollbackFailed = register("transaction_rollback_failed", "failed to rollback transaction", nil) // // Recovery operations // // ErrRecoveryFailed indicates the recovery operation failed. var ErrRecoveryFailed = register("recovery_failed", "recovery failed", nil) // ErrRecoveryRetryFailed indicates recovery retry operation failed. var ErrRecoveryRetryFailed = register("recovery_retry_failed", "recovery retry failed", nil) // ErrRecoveryRetryLimitReached indicates the recovery retry limit was reached. var ErrRecoveryRetryLimitReached = register("recovery_retry_limit_reached", "recovery retry limit reached", nil) // // Retry operations // // ErrRetryContextCanceled indicates the retry was canceled due to context cancellation. var ErrRetryContextCanceled = register("retry_context_canceled", "retry canceled due to context cancellation", nil) // ErrRetryMaxElapsedTimeReached indicates the maximum elapsed time for retries was reached. var ErrRetryMaxElapsedTimeReached = register("retry_max_elapsed_time_reached", "maximum elapsed time for retries reached", nil) // ErrRetryOperationFailed indicates the retry operation failed. var ErrRetryOperationFailed = register("retry_operation_failed", "retry operation failed", nil) // // X509/SPIFFE // // ErrSPIFFEEmptyTrustDomain indicates an empty SPIFFE trust domain was provided. var ErrSPIFFEEmptyTrustDomain = register("spiffe_empty_trust_domain", "empty trust domain", nil) // ErrSPIFFEFailedToCloseX509Source indicates X509Source close operation failed. var ErrSPIFFEFailedToCloseX509Source = register("spiffe_failed_to_close_source", "failed to close X509Source", nil) // ErrSPIFFEFailedToExtractX509SVID indicates X509 SVID extraction failed. var ErrSPIFFEFailedToExtractX509SVID = register("spiffe_failed_to_extract_x509_svid", "failed to extract X509 SVID", nil) // ErrSPIFFEInvalidSPIFFEID indicates an invalid SPIFFE ID was provided. var ErrSPIFFEInvalidSPIFFEID = register("spiffe_invalid_spiffe_id", "invalid SPIFFE ID", nil) // ErrSPIFFEInvalidTrustDomain indicates an invalid trust domain was provided. var ErrSPIFFEInvalidTrustDomain = register("spiffe_invalid_trust_domain", "invalid trust domain", nil) // ErrSPIFFEMultipleTrustDomains indicates multiple trust domains were provided when only one is allowed. var ErrSPIFFEMultipleTrustDomains = register("spiffe_multiple_trust_domains", "provide a single trust domain", nil) // ErrSPIFFENilX509Source indicates a nil X509Source was provided. var ErrSPIFFENilX509Source = register("spiffe_nil_x509_source", "nil X509Source", nil) // ErrSPIFFENoPeerCertificates indicates no peer certificates were found. var ErrSPIFFENoPeerCertificates = register("spiffe_no_peer_certificates", "no peer certificates", nil) // ErrSPIFFEUnableToFetchX509Source indicates unable to fetch X509Source. var ErrSPIFFEUnableToFetchX509Source = register("spiffe_unable_to_fetch_x509_source", "unable to fetch X509Source", nil) spike-sdk-go-0.16.4/go.mod000066400000000000000000000016311511163700700152070ustar00rootroot00000000000000module github.com/spiffe/spike-sdk-go go 1.25.3 require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudflare/circl v1.6.1 github.com/google/uuid v1.6.0 github.com/spiffe/go-spiffe/v2 v2.6.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/bwesterb/go-ristretto v1.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) spike-sdk-go-0.16.4/go.sum000066400000000000000000000131501511163700700152330ustar00rootroot00000000000000github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= spike-sdk-go-0.16.4/hack/000077500000000000000000000000001511163700700150065ustar00rootroot00000000000000spike-sdk-go-0.16.4/hack/coverage-report.sh000077500000000000000000000066661511163700700204670ustar00rootroot00000000000000#!/usr/bin/env bash # \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ # \\\\\ Copyright 2024-present SPIKE contributors. # \\\\\\\ SPDX-License-Identifier: Apache-2.0 # This script generates an HTML coverage report for the SPIKE SDK Go codebase # and publishes it to the documentation directory. # # Usage: ./hack/coverage-report.sh # # Output: $HOME/WORKSPACE/spike/docs/sdk/coverage.html # # The script performs the following steps: # 1. Runs all tests with coverage profiling # 2. Generates an HTML coverage report # 3. Copies the report to the documentation directory # 4. Displays a summary of the coverage results set -euo pipefail # Color codes for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # Paths readonly COVERAGE_OUT="/tmp/spike-sdk-go-coverage.out" readonly COVERAGE_HTML="/tmp/spike-sdk-go-coverage.html" readonly DOCS_DIR="${HOME}/WORKSPACE/spike/docs/sdk" readonly TARGET_HTML="${DOCS_DIR}/coverage.html" echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║ SPIKE SDK Go - Coverage Report Generator ║${NC}" echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" # Step 1: Run tests with coverage echo -e "${YELLOW}📊 Running tests with coverage profiling...${NC}" if go test -v -race -buildvcs -coverprofile="${COVERAGE_OUT}" ./...; then echo -e "${GREEN}✓ Tests completed successfully${NC}" else echo -e "${RED}✗ Tests failed${NC}" exit 1 fi echo "" # Step 2: Generate HTML coverage report echo -e "${YELLOW}🔨 Generating HTML coverage report...${NC}" if go tool cover -html="${COVERAGE_OUT}" -o="${COVERAGE_HTML}"; then echo -e "${GREEN}✓ HTML report generated: ${COVERAGE_HTML}${NC}" else echo -e "${RED}✗ Failed to generate HTML report${NC}" exit 1 fi echo "" # Step 3: Create docs directory if it doesn't exist if [ ! -d "${DOCS_DIR}" ]; then echo -e "${YELLOW}📁 Creating documentation directory: ${DOCS_DIR}${NC}" mkdir -p "${DOCS_DIR}" fi # Step 4: Copy report to documentation directory echo -e "${YELLOW}📤 Publishing report to documentation...${NC}" if cp "${COVERAGE_HTML}" "${TARGET_HTML}"; then echo -e "${GREEN}✓ Coverage report published to: ${TARGET_HTML}${NC}" else echo -e "${RED}✗ Failed to publish coverage report${NC}" exit 1 fi echo "" # Step 5: Display coverage summary echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}Coverage Summary:${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # Extract and display coverage percentages go tool cover -func="${COVERAGE_OUT}" | tail -1 echo "" echo -e "${GREEN}✓ Coverage report generation complete!${NC}" echo -e "${BLUE} View the report at: file://${TARGET_HTML}${NC}" echo "" spike-sdk-go-0.16.4/hack/tag.sh000077500000000000000000000004151511163700700161200ustar00rootroot00000000000000#!/usr/bin/env bash # \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ # \\\\\ Copyright 2024-present SPIKE contributors. # \\\\\\\ SPDX-License-Identifier: Apache-2.0 VERSION="v0.16.4" git tag -s "$VERSION" -m "$VERSION" git push origin --tags spike-sdk-go-0.16.4/kv/000077500000000000000000000000001511163700700145205ustar00rootroot00000000000000spike-sdk-go-0.16.4/kv/delete.go000066400000000000000000000061531511163700700163160ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "time" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // Delete marks secret versions as deleted for a given path. The deletion is // performed by setting the DeletedTime to the current time. // // IMPORTANT: This is a soft delete. The path remains in the store even if all // versions are deleted. To completely remove a path and reclaim memory, use // Destroy() after deleting all versions. // // The function supports flexible version deletion with the following behavior: // - If versions is empty, deletes only the current version // - If versions contains specific numbers, deletes those versions // - Version 0 in the array represents the current version // - Non-existent versions are silently skipped without error // // This idempotent behavior is useful for batch operations where you want to // ensure certain versions are deleted without failing if some don't exist. // // Parameters: // - path: Path to the secret to delete // - versions: Array of version numbers to delete (empty array deletes current // version only, 0 in the array represents current version) // // Returns: // - []int: Array of version numbers that were actually modified (had their // DeletedTime changed from nil to now). Already-deleted versions are not // included in this list. // - *errors.SDKError: nil on success, or one of the following sdkErrors: // - ErrEntityNotFound: if the path doesn't exist // // Example: // // // Delete current version only // modified, err := kv.Delete("secret/path", []int{}) // if err != nil { // log.Printf("Failed to delete secret: %v", err) // } // log.Printf("Deleted %d version(s): %v", len(modified), modified) // // // Delete specific versions // modified, err = kv.Delete("secret/path", []int{1, 2, 3}) // if err != nil { // log.Printf("Failed to delete versions: %v", err) // } // log.Printf("Actually deleted: %v", modified) func (kv *KV) Delete(path string, versions []int) ([]int, *sdkErrors.SDKError) { secret, exists := kv.data[path] if !exists { return nil, sdkErrors.ErrEntityNotFound } now := time.Now() cv := secret.Metadata.CurrentVersion var modified []int // If no versions specified, mark the latest version as deleted if len(versions) == 0 { if v, exists := secret.Versions[cv]; exists && v.DeletedTime == nil { v.DeletedTime = &now // Mark as deleted. secret.Versions[cv] = v modified = append(modified, cv) } return modified, nil } // Delete specific versions for _, version := range versions { if version == 0 { v, exists := secret.Versions[cv] if !exists || v.DeletedTime != nil { continue } v.DeletedTime = &now // Mark as deleted. secret.Versions[cv] = v modified = append(modified, cv) continue } if v, exists := secret.Versions[version]; exists && v.DeletedTime == nil { v.DeletedTime = &now // Mark as deleted. secret.Versions[version] = v modified = append(modified, version) } } return modified, nil } spike-sdk-go-0.16.4/kv/delete_modified_test.go000066400000000000000000000161511511163700700212140ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "testing" "time" ) // TestKV_Delete_ModifiedReturnValue tests the modified versions return value // to ensure Delete properly reports which versions were actually modified. func TestKV_Delete_ModifiedReturnValue(t *testing.T) { tests := []struct { name string setup func() *KV path string versions []int wantModified []int wantErr bool }{ { name: "delete current version returns modified", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "value"}) return kv }, path: "test/path", versions: []int{}, wantModified: []int{1}, // Current version is 1 wantErr: false, }, { name: "delete already deleted returns empty", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "value"}) _, _ = kv.Delete("test/path", []int{1}) // Delete it first return kv }, path: "test/path", versions: []int{1}, wantModified: []int{}, // Already deleted, no modifications wantErr: false, }, { name: "delete multiple versions returns all modified", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "v1"}) kv.Put("test/path", map[string]string{"key": "v2"}) kv.Put("test/path", map[string]string{"key": "v3"}) return kv }, path: "test/path", versions: []int{1, 2, 3}, wantModified: []int{1, 2, 3}, wantErr: false, }, { name: "delete mix of existing and non-existing returns only existing", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "v1"}) kv.Put("test/path", map[string]string{"key": "v2"}) return kv }, path: "test/path", versions: []int{1, 2, 99, 100}, // 99, 100 don't exist wantModified: []int{1, 2}, wantErr: false, }, { name: "delete version 0 returns current version number", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "v1"}) kv.Put("test/path", map[string]string{"key": "v2"}) kv.Put("test/path", map[string]string{"key": "v3"}) return kv }, path: "test/path", versions: []int{0}, // 0 means current wantModified: []int{3}, // Current is version 3 wantErr: false, }, { name: "delete non-existent versions returns empty", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "v1"}) return kv }, path: "test/path", versions: []int{99, 100, 101}, wantModified: []int{}, wantErr: false, }, { name: "delete some already deleted returns only newly deleted", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "v1"}) kv.Put("test/path", map[string]string{"key": "v2"}) kv.Put("test/path", map[string]string{"key": "v3"}) _, _ = kv.Delete("test/path", []int{1}) // Pre-delete version 1 return kv }, path: "test/path", versions: []int{1, 2, 3}, // 1 already deleted wantModified: []int{2, 3}, // Only 2 and 3 are newly deleted wantErr: false, }, { name: "delete empty versions on empty current returns empty", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } // Manually create state where current version doesn't exist kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{}, // No versions! } return kv }, path: "test/path", versions: []int{}, wantModified: []int{}, // Current doesn't exist, nothing modified wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() modified, err := kv.Delete(tt.path, tt.versions) if tt.wantErr { if err == nil { t.Error("Delete() expected error, got nil") } return } if err != nil { t.Errorf("Delete() unexpected error: %v", err) return } // Check modified length if len(modified) != len(tt.wantModified) { t.Errorf("Delete() modified length = %d, want %d (got: %v, want: %v)", len(modified), len(tt.wantModified), modified, tt.wantModified) return } // Check modified contains all expected versions modifiedMap := make(map[int]bool) for _, v := range modified { modifiedMap[v] = true } for _, wantVer := range tt.wantModified { if !modifiedMap[wantVer] { t.Errorf("Delete() modified missing version %d, got: %v, want: %v", wantVer, modified, tt.wantModified) } } }) } } // TestKV_Delete_StateVerification verifies internal state consistency // after delete operations. func TestKV_Delete_StateVerification(t *testing.T) { t.Run("deleted version has DeletedTime set", func(t *testing.T) { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "value"}) beforeTime := time.Now() modified, err := kv.Delete("test/path", []int{1}) afterTime := time.Now() if err != nil { t.Fatalf("Delete() error = %v", err) } if len(modified) != 1 { t.Fatalf("modified length = %d, want 1", len(modified)) } secret := kv.data["test/path"] version := secret.Versions[1] if version.DeletedTime == nil { t.Error("DeletedTime should be set after delete") } // Verify DeletedTime is reasonable if version.DeletedTime.Before(beforeTime) || version.DeletedTime.After(afterTime) { t.Errorf("DeletedTime %v not between %v and %v", version.DeletedTime, beforeTime, afterTime) } }) t.Run("second delete of same version returns empty modified", func(t *testing.T) { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "value"}) // First delete modified1, err := kv.Delete("test/path", []int{1}) if err != nil { t.Fatalf("First Delete() error = %v", err) } if len(modified1) != 1 { t.Errorf("First delete modified = %d, want 1", len(modified1)) } // Second delete (idempotent) modified2, err := kv.Delete("test/path", []int{1}) if err != nil { t.Fatalf("Second Delete() error = %v", err) } if len(modified2) != 0 { t.Errorf("Second delete modified = %d, want 0 (idempotent)", len(modified2)) } }) t.Run("data still accessible via GetRawSecret after delete", func(t *testing.T) { kv := New(Config{MaxSecretVersions: 10}) kv.Put("test/path", map[string]string{"key": "value"}) _, err := kv.Delete("test/path", []int{1}) if err != nil { t.Fatalf("Delete() error = %v", err) } // GetRawSecret should still return the data secret, err := kv.GetRawSecret("test/path") if err != nil { t.Errorf("GetRawSecret() error = %v", err) } if secret == nil { t.Error("GetRawSecret() should return secret even if deleted") } }) } spike-sdk-go-0.16.4/kv/delete_test.go000066400000000000000000000115731511163700700173570ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "errors" "testing" "time" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) func TestKV_Delete(t *testing.T) { tests := []struct { name string setup func() *KV path string versions []int wantErr error }{ { name: "non_existent_path", setup: func() *KV { return &KV{ data: make(map[string]*Value), } }, path: "non/existent/path", versions: nil, wantErr: sdkErrors.ErrEntityNotFound, }, { name: "delete_current_version_no_versions_specified", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "test_value", }, }, }, } return kv }, path: "test/path", versions: nil, wantErr: nil, }, { name: "delete_specific_versions", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 2, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "value1", }, }, 2: { Data: map[string]string{ "key": "value2", }, }, }, } return kv }, path: "test/path", versions: []int{1, 2}, wantErr: nil, }, { name: "delete_version_0_when_current_version_does_not_exist", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } // Create secret with CurrentVersion=2 but version 2 doesn't exist kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 2, OldestVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value1"}, }, // Version 2 missing! }, } return kv }, path: "test/path", versions: []int{0}, // 0 means current version wantErr: nil, }, { name: "delete_already_deleted_version_idempotent", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } deletedTime := time.Now() kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, OldestVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, DeletedTime: &deletedTime, // Already deleted }, }, } return kv }, path: "test/path", versions: []int{1}, wantErr: nil, }, { name: "delete_non_existent_version_silent_skip", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, OldestVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, }, }, } return kv }, path: "test/path", versions: []int{99}, // Version doesn't exist wantErr: nil, }, { name: "delete_empty_versions_when_current_version_missing", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } // Create secret with CurrentVersion but that version doesn't exist kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 5, OldestVersion: 1, }, Versions: map[int]Version{ // Version 5 is missing }, } return kv }, path: "test/path", versions: []int{}, // Empty means delete current wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() _, err := kv.Delete(tt.path, tt.versions) // Handle nil case explicitly to avoid typed nil vs untyped nil issues if tt.wantErr == nil { if err != nil { t.Errorf("Delete() error = %v, wantErr nil", err) return } } else if !errors.Is(err, tt.wantErr) { t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { secret, exists := kv.data[tt.path] if !exists { t.Errorf("Value should still exist after deletion") return } if len(tt.versions) == 0 { cv := secret.Metadata.CurrentVersion if v, exists := secret.Versions[cv]; exists { if v.DeletedTime == nil { t.Errorf("Current version should be marked as deleted") } } } else { for _, version := range tt.versions { if version == 0 { version = secret.Metadata.CurrentVersion } if v, exists := secret.Versions[version]; exists { if v.DeletedTime == nil { t.Errorf("Version %d should be marked as deleted", version) } } } } } }) } } spike-sdk-go-0.16.4/kv/destroy.go000066400000000000000000000030741511163700700165440ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // Destroy permanently removes a secret path from the store, including all // versions (both active and deleted). This is a hard delete operation that // cannot be undone. // // Unlike Delete(), which soft-deletes versions by marking them with // DeletedTime, Destroy() completely removes the path from the internal map, // reclaiming the memory. // // This operation is useful for: // - Purging secrets that have all versions deleted // - Removing obsolete paths to prevent unbounded map growth // - Compliance requirements for data removal // // Parameters: // - path: The path to permanently remove from the store // // Returns: // - *sdkErrors.SDKError: ErrEntityNotFound if the path does not exist, // nil on success // // Example: // // // Delete all versions first // kv.Delete("secret/path", []int{}) // // // Check if empty and destroy // secret, _ := kv.GetRawSecret("secret/path") // if secret.IsEmpty() { // err := kv.Destroy("secret/path") // if err != nil { // log.Printf("Failed to destroy secret: %v", err) // } // } // // // Or destroy directly (removes regardless of deletion state) // err := kv.Destroy("secret/path") func (kv *KV) Destroy(path string) *sdkErrors.SDKError { if _, exists := kv.data[path]; !exists { return sdkErrors.ErrEntityNotFound } delete(kv.data, path) return nil } spike-sdk-go-0.16.4/kv/doc.go000066400000000000000000000007451511163700700156220ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package kv provides a secure in-memory key-value store for managing secret // data. The store supports versioning of secrets, allowing operations on // specific versions and tracking deleted versions. It is designed for scenarios // where secrets need to be securely managed, updated, and deleted. package kv spike-sdk-go-0.16.4/kv/entity.go000066400000000000000000000061121511163700700163630ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import "time" // Version represents a single version of versioned data along with its // metadata. Each version maintains its own set of key-value pairs and tracking // information. type Version struct { // Data contains the actual key-value pairs stored in this version Data map[string]string // CreatedTime is when this version was created CreatedTime time.Time // Version is the numeric identifier for this version. Version numbers // start at 1 and increment with each update. Version int // DeletedTime indicates when this version was marked as deleted // A nil value means the version is active/not deleted DeletedTime *time.Time } // Metadata tracks control information for versioned data stored at a path. // It maintains version boundaries and timestamps for the overall data // collection. type Metadata struct { // CurrentVersion is the newest/latest non-deleted version number. // Version numbers start at 1. A value of 0 indicates that all versions // have been deleted (no valid version exists). CurrentVersion int // OldestVersion is the oldest available version number OldestVersion int // CreatedTime is when the data at this path was first created CreatedTime time.Time // UpdatedTime is when the data was last modified UpdatedTime time.Time // MaxVersions is the maximum number of versions to retain // When exceeded, older versions are automatically pruned MaxVersions int } // Value represents a versioned collection of key-value pairs stored at a // specific path. It maintains both the version history and metadata about the // collection as a whole. type Value struct { // Versions maps version numbers to their corresponding Version objects Versions map[int]Version // Metadata contains control information about this versioned data Metadata Metadata } // HasValidVersions returns true if the Value has at least one non-deleted // version. It iterates through all versions to check their DeletedTime. // // Returns: // - true if any version has DeletedTime == nil (active version exists) // - false if all versions are deleted or no versions exist // // Note: This method performs a full scan of all versions. For stores where // CurrentVersion is maintained correctly (like SPIKE Nexus), checking // Metadata.CurrentVersion != 0 is more efficient. func (v *Value) HasValidVersions() bool { for _, version := range v.Versions { if version.DeletedTime == nil { return true } } return false } // Empty returns true if the Value has no valid (non-deleted) versions. // This is the inverse of HasValidVersions() and is useful for identifying // secrets that can be purged from storage. // // Returns: // - true if all versions are deleted or no versions exist // - false if at least one active version exists // // Example: // // if secret.IsEmpty() { // // Safe to remove from storage // kv.Destroy(path) // } func (v *Value) Empty() bool { return !v.HasValidVersions() } spike-sdk-go-0.16.4/kv/get.go000066400000000000000000000055671511163700700156430ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import sdkErrors "github.com/spiffe/spike-sdk-go/errors" // Get retrieves a versioned key-value data map from the store at the specified // path. // // The function supports versioned data retrieval with the following behavior: // - If version is 0, returns the current version of the data // - If version is specified, returns that specific version if it exists // - Returns nil if the path doesn't exist // - Returns nil if the specified version doesn't exist // - Returns nil if the version has been deleted (DeletedTime is set) // // Parameters: // - path: The path to retrieve data from // - version: The specific version to retrieve (0 for current version) // // Returns: // - map[string]string: The key-value data at the specified path and version, // nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrEntityNotFound: if the path doesn't exist // - ErrEntityDeleted: if the version doesn't exist or has been deleted // // Example: // // // Get current version // data, err := kv.Get("secret/myapp", 0) // if err != nil { // log.Printf("Failed to get secret: %v", err) // return // } // // // Get specific version // historicalData, err := kv.Get("secret/myapp", 2) // if err != nil { // log.Printf("Failed to get version 2: %v", err) // return // } func (kv *KV) Get(path string, version int) (map[string]string, *sdkErrors.SDKError) { secret, exists := kv.data[path] if !exists { return nil, sdkErrors.ErrEntityNotFound } // If the version not specified, use the current version: if version == 0 { version = secret.Metadata.CurrentVersion } v, exists := secret.Versions[version] if !exists || v.DeletedTime != nil { return nil, sdkErrors.ErrEntityDeleted } return v.Data, nil } // GetRawSecret retrieves a raw secret from the store at the specified path. // This function is similar to Get, but it returns the raw Value object instead // of the key-value data map, providing access to all versions and metadata. // // Parameters: // - path: The path to retrieve the secret from // // Returns: // - *Value: The complete secret object with all versions and metadata, nil // on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrEntityNotFound: if the path doesn't exist // // Example: // // secret, err := kv.GetRawSecret("secret/myapp") // if err != nil { // log.Printf("Failed to get raw secret: %v", err) // return // } // log.Printf("Current version: %d", secret.Metadata.CurrentVersion) func (kv *KV) GetRawSecret(path string) (*Value, *sdkErrors.SDKError) { secret, exists := kv.data[path] if !exists { return nil, sdkErrors.ErrEntityNotFound } return secret, nil } spike-sdk-go-0.16.4/kv/get_test.go000066400000000000000000000141651511163700700166740ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "errors" "testing" "time" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) func TestKV_Get(t *testing.T) { tests := []struct { name string setup func() *KV path string version int want map[string]string wantErr error }{ { name: "non_existent_path", setup: func() *KV { return &KV{ data: make(map[string]*Value), } }, path: "non/existent/path", version: 0, want: nil, wantErr: sdkErrors.ErrEntityNotFound, }, { name: "get_current_version", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "current_value", }, Version: 1, }, }, } return kv }, path: "test/path", version: 0, want: map[string]string{ "key": "current_value", }, wantErr: nil, }, { name: "get_specific_version", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 2, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "old_value", }, Version: 1, }, 2: { Data: map[string]string{ "key": "current_value", }, Version: 2, }, }, } return kv }, path: "test/path", version: 1, want: map[string]string{ "key": "old_value", }, wantErr: nil, }, { name: "get_deleted_version", setup: func() *KV { deletedTime := time.Now() kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "deleted_value", }, Version: 1, DeletedTime: &deletedTime, }, }, } return kv }, path: "test/path", version: 1, want: nil, wantErr: sdkErrors.ErrEntityDeleted, }, { name: "non_existent_version", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "value", }, Version: 1, }, }, } return kv }, path: "test/path", version: 999, want: nil, wantErr: sdkErrors.ErrEntityDeleted, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() got, err := kv.Get(tt.path, tt.version) // Handle nil case explicitly to avoid typed nil vs untyped nil issues if tt.wantErr == nil { if err != nil { t.Errorf("Get() error = %v, wantErr nil", err) return } } else if !errors.Is(err, tt.wantErr) { t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { if len(got) != len(tt.want) { t.Errorf("Get() got = %v, want %v", got, tt.want) return } for k, v := range got { if tt.want[k] != v { t.Errorf("Get() got[%s] = %v, want[%s] = %v", k, v, k, tt.want[k]) } } } }) } } func TestKV_GetRawSecret(t *testing.T) { tests := []struct { name string setup func() *KV path string want *Value wantErr error }{ { name: "non_existent_path", setup: func() *KV { return &KV{ data: make(map[string]*Value), } }, path: "non/existent/path", want: nil, wantErr: sdkErrors.ErrEntityNotFound, }, { name: "existing_secret", setup: func() *KV { secret := &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "value", }, Version: 1, }, }, } kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = secret return kv }, path: "test/path", want: &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "value", }, Version: 1, }, }, }, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() got, err := kv.GetRawSecret(tt.path) // Handle nil case explicitly to avoid typed nil vs untyped nil issues if tt.wantErr == nil { if err != nil { t.Errorf("GetRawSecret() error = %v, wantErr nil", err) return } } else if !errors.Is(err, tt.wantErr) { t.Errorf("GetRawSecret() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { if got.Metadata.CurrentVersion != tt.want.Metadata.CurrentVersion { t.Errorf("GetRawSecret() got CurrentVersion = %v, want %v", got.Metadata.CurrentVersion, tt.want.Metadata.CurrentVersion) } if len(got.Versions) != len(tt.want.Versions) { t.Errorf("GetRawSecret() got Versions length = %v, want %v", len(got.Versions), len(tt.want.Versions)) return } for version, gotV := range got.Versions { wantV, exists := tt.want.Versions[version] if !exists { t.Errorf("GetRawSecret() unexpected version %v in result", version) continue } if gotV.Version != wantV.Version { t.Errorf("GetRawSecret() version %v: got Version = %v, want %v", version, gotV.Version, wantV.Version) } if len(gotV.Data) != len(wantV.Data) { t.Errorf("GetRawSecret() version %v: got Data length = %v, want %v", version, len(gotV.Data), len(wantV.Data)) continue } for k, v := range gotV.Data { if wantV.Data[k] != v { t.Errorf("GetRawSecret() version %v: got Data[%s] = %v, want %v", version, k, v, wantV.Data[k]) } } } } }) } } spike-sdk-go-0.16.4/kv/import.go000066400000000000000000000044731511163700700163710ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv // ImportSecrets hydrates the key-value store with secrets loaded from // persistent storage or a similar medium. It takes a map of path to secret // values and adds them to the in-memory store. This is typically used during // initialization or recovery after a system crash. // // The method performs a deep copy of all imported secrets to avoid sharing // memory between the source data and the KV store. If a secret already exists // in the store, it will be overwritten with the imported value. All version // history and metadata from the imported secrets are preserved. // // Parameters: // - secrets: Map of secret paths to their complete Value objects (including // all versions and metadata) // // Returns: // - None // // Example: // // secrets, err := persistentStore.LoadAllSecrets(context.Background()) // if err != nil { // log.Fatalf("Failed to load secrets: %v", err) // } // kv.ImportSecrets(secrets) // log.Printf("Imported %d secrets", len(secrets)) func (kv *KV) ImportSecrets(secrets map[string]*Value) { for path, secret := range secrets { // Create a deep copy of the secret to avoid sharing memory newSecret := &Value{ Versions: make(map[int]Version, len(secret.Versions)), Metadata: Metadata{ CreatedTime: secret.Metadata.CreatedTime, UpdatedTime: secret.Metadata.UpdatedTime, MaxVersions: kv.maxSecretVersions, // Use the KV store's setting CurrentVersion: secret.Metadata.CurrentVersion, OldestVersion: secret.Metadata.OldestVersion, }, } // Copy all versions for versionNum, version := range secret.Versions { // Deep copy the data map dataCopy := make(map[string]string, len(version.Data)) for k, v := range version.Data { dataCopy[k] = v } // Create the version copy versionCopy := Version{ Data: dataCopy, CreatedTime: version.CreatedTime, Version: versionNum, } // Copy deleted time if set if version.DeletedTime != nil { deletedTime := *version.DeletedTime versionCopy.DeletedTime = &deletedTime } newSecret.Versions[versionNum] = versionCopy } // Store the copied secret kv.data[path] = newSecret } } spike-sdk-go-0.16.4/kv/import_test.go000066400000000000000000000171031511163700700174220ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "testing" "time" ) func TestKV_ImportSecrets(t *testing.T) { tests := []struct { name string setup func() *KV secrets map[string]*Value verifyFunc func(*testing.T, *KV) }{ { name: "import single secret with one version", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: map[string]*Value{ "app/config": { Metadata: Metadata{ CurrentVersion: 1, OldestVersion: 1, CreatedTime: time.Now(), UpdatedTime: time.Now(), MaxVersions: 5, // Will be overridden to 10 }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, Version: 1, CreatedTime: time.Now(), }, }, }, }, verifyFunc: func(t *testing.T, kv *KV) { secret, exists := kv.data["app/config"] if !exists { t.Fatal("imported secret not found") } if secret.Metadata.MaxVersions != 10 { t.Errorf("MaxVersions = %d, want 10 (KV config should override)", secret.Metadata.MaxVersions) } if len(secret.Versions) != 1 { t.Errorf("version count = %d, want 1", len(secret.Versions)) } if secret.Versions[1].Data["key"] != "value" { t.Errorf("data not imported correctly") } }, }, { name: "import secret with multiple versions", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: map[string]*Value{ "app/db": { Metadata: Metadata{ CurrentVersion: 3, OldestVersion: 1, CreatedTime: time.Now(), UpdatedTime: time.Now(), MaxVersions: 10, }, Versions: map[int]Version{ 1: { Data: map[string]string{"host": "localhost"}, Version: 1, CreatedTime: time.Now(), }, 2: { Data: map[string]string{"host": "db.example.com"}, Version: 2, CreatedTime: time.Now(), }, 3: { Data: map[string]string{"host": "db.prod.com"}, Version: 3, CreatedTime: time.Now(), }, }, }, }, verifyFunc: func(t *testing.T, kv *KV) { secret, exists := kv.data["app/db"] if !exists { t.Fatal("imported secret not found") } if len(secret.Versions) != 3 { t.Errorf("version count = %d, want 3", len(secret.Versions)) } if secret.Metadata.CurrentVersion != 3 { t.Errorf("CurrentVersion = %d, want 3", secret.Metadata.CurrentVersion) } if secret.Versions[3].Data["host"] != "db.prod.com" { t.Errorf("latest version data incorrect") } }, }, { name: "import secret with deleted version", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: func() map[string]*Value { deletedTime := time.Now().Add(-1 * time.Hour) return map[string]*Value{ "app/cache": { Metadata: Metadata{ CurrentVersion: 2, OldestVersion: 1, CreatedTime: time.Now(), UpdatedTime: time.Now(), MaxVersions: 10, }, Versions: map[int]Version{ 1: { Data: map[string]string{"ttl": "300"}, Version: 1, CreatedTime: time.Now(), DeletedTime: &deletedTime, }, 2: { Data: map[string]string{"ttl": "600"}, Version: 2, CreatedTime: time.Now(), }, }, }, } }(), verifyFunc: func(t *testing.T, kv *KV) { secret, exists := kv.data["app/cache"] if !exists { t.Fatal("imported secret not found") } v1 := secret.Versions[1] if v1.DeletedTime == nil { t.Error("deleted version should have DeletedTime set") } v2 := secret.Versions[2] if v2.DeletedTime != nil { t.Error("active version should not have DeletedTime set") } }, }, { name: "import overwrites existing secret", setup: func() *KV { kv := New(Config{MaxSecretVersions: 10}) kv.Put("app/config", map[string]string{"old": "data"}) return kv }, secrets: map[string]*Value{ "app/config": { Metadata: Metadata{ CurrentVersion: 1, OldestVersion: 1, CreatedTime: time.Now(), UpdatedTime: time.Now(), MaxVersions: 10, }, Versions: map[int]Version{ 1: { Data: map[string]string{"new": "imported"}, Version: 1, CreatedTime: time.Now(), }, }, }, }, verifyFunc: func(t *testing.T, kv *KV) { secret, exists := kv.data["app/config"] if !exists { t.Fatal("secret not found") } if _, oldExists := secret.Versions[1].Data["old"]; oldExists { t.Error("old data should be overwritten") } if secret.Versions[1].Data["new"] != "imported" { t.Error("new data not imported") } }, }, { name: "import empty secrets map", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: map[string]*Value{}, verifyFunc: func(t *testing.T, kv *KV) { if len(kv.data) != 0 { t.Errorf("store should be empty, got %d secrets", len(kv.data)) } }, }, { name: "deep copy verification - no memory sharing", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: map[string]*Value{ "app/test": { Metadata: Metadata{ CurrentVersion: 1, OldestVersion: 1, CreatedTime: time.Now(), UpdatedTime: time.Now(), MaxVersions: 10, }, Versions: map[int]Version{ 1: { Data: map[string]string{"shared": "original"}, Version: 1, CreatedTime: time.Now(), }, }, }, }, verifyFunc: func(t *testing.T, kv *KV) { // Modify the original data (which we'll pass to verify we copied) // This test needs the original secrets map to verify no sharing // We'll verify by modifying KV data and checking it doesn't affect anything secret := kv.data["app/test"] secret.Versions[1].Data["shared"] = "modified" // If we had a reference to original, this would affect it // Since we don't have access to original here, we verify the copy exists if secret.Versions[1].Data["shared"] != "modified" { t.Error("should be able to modify imported data independently") } }, }, { name: "import multiple secrets", setup: func() *KV { return New(Config{MaxSecretVersions: 10}) }, secrets: map[string]*Value{ "app/config": { Metadata: Metadata{CurrentVersion: 1, OldestVersion: 1}, Versions: map[int]Version{ 1: {Data: map[string]string{"key": "value1"}, Version: 1}, }, }, "app/db": { Metadata: Metadata{CurrentVersion: 1, OldestVersion: 1}, Versions: map[int]Version{ 1: {Data: map[string]string{"key": "value2"}, Version: 1}, }, }, "app/cache": { Metadata: Metadata{CurrentVersion: 1, OldestVersion: 1}, Versions: map[int]Version{ 1: {Data: map[string]string{"key": "value3"}, Version: 1}, }, }, }, verifyFunc: func(t *testing.T, kv *KV) { if len(kv.data) != 3 { t.Errorf("imported %d secrets, want 3", len(kv.data)) } paths := []string{"app/config", "app/db", "app/cache"} for _, path := range paths { if _, exists := kv.data[path]; !exists { t.Errorf("secret %s not imported", path) } } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() kv.ImportSecrets(tt.secrets) tt.verifyFunc(t, kv) }) } } spike-sdk-go-0.16.4/kv/kv.go000066400000000000000000000052021511163700700154660ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package kv provides an in-memory key-value store with automatic versioning // and bounded cache semantics. // // # Concurrency Safety // // This package is NOT safe for concurrent use. All methods on KV must be // externally synchronized. Callers are responsible for providing appropriate // locking mechanisms (e.g., sync.RWMutex) to protect concurrent access. // // Concurrent operations without synchronization will cause data races and // undefined behavior. // // Example of safe concurrent usage: // // type SafeStore struct { // kv *kv.KV // mu sync.RWMutex // } // // func (s *SafeStore) Put(path string, data map[string]string) { // s.mu.Lock() // defer s.mu.Unlock() // s.kv.Put(path, data) // } // // func (s *SafeStore) Get(path string, version int) (map[string]string, error) { // s.mu.RLock() // defer s.mu.RUnlock() // return s.kv.Get(path, version) // } // // Use sync.RWMutex to allow concurrent reads while serializing writes for // optimal performance. package kv // KV represents an in-memory key-value store with automatic versioning and // bounded cache semantics. Each path maintains a configurable maximum number // of versions, with older versions automatically pruned when the limit is // exceeded. // // The store supports: // - Versioned storage with automatic version numbering // - Soft deletion with undelete capability // - Bounded cache with automatic pruning of old versions // - Version-specific retrieval and metadata tracking type KV struct { maxSecretVersions int data map[string]*Value } // Config represents the configuration for a KV instance. type Config struct { // MaxSecretVersions is the maximum number of versions to retain per path. // When exceeded, older versions are automatically pruned. // Must be positive. A typical value is 10. MaxSecretVersions int } // New creates a new KV instance with the specified configuration. // // The store is initialized as an empty in-memory key-value store with // versioning enabled. All paths stored in this instance will retain up to // MaxSecretVersions versions. // // Parameters: // - config: Configuration specifying MaxSecretVersions // // Returns: // - *KV: A new KV instance ready for use // // Example: // // kv := New(Config{MaxSecretVersions: 10}) // kv.Put("app/config", map[string]string{"key": "value"}) func New(config Config) *KV { return &KV{ maxSecretVersions: config.MaxSecretVersions, data: make(map[string]*Value), } } spike-sdk-go-0.16.4/kv/kv_test.go000066400000000000000000000053311511163700700165300ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "testing" ) func TestNew(t *testing.T) { tests := []struct { name string config Config verify func(*testing.T, *KV) }{ { name: "basic construction with typical MaxSecretVersions", config: Config{ MaxSecretVersions: 10, }, verify: func(t *testing.T, kv *KV) { if kv == nil { t.Fatal("New() returned nil") return } if kv.maxSecretVersions != 10 { t.Errorf("maxSecretVersions = %d, want 10", kv.maxSecretVersions) } if kv.data == nil { t.Error("data map not initialized") } if len(kv.data) != 0 { t.Errorf("data map should be empty, got %d entries", len(kv.data)) } }, }, { name: "construction with MaxSecretVersions=1", config: Config{ MaxSecretVersions: 1, }, verify: func(t *testing.T, kv *KV) { if kv.maxSecretVersions != 1 { t.Errorf("maxSecretVersions = %d, want 1", kv.maxSecretVersions) } // Verify it works correctly by doing a Put kv.Put("test", map[string]string{"key": "v1"}) kv.Put("test", map[string]string{"key": "v2"}) secret := kv.data["test"] if len(secret.Versions) != 1 { t.Errorf("with MaxVersions=1, should only keep 1 version, got %d", len(secret.Versions)) } }, }, { name: "construction with large MaxSecretVersions", config: Config{ MaxSecretVersions: 1000, }, verify: func(t *testing.T, kv *KV) { if kv.maxSecretVersions != 1000 { t.Errorf("maxSecretVersions = %d, want 1000", kv.maxSecretVersions) } }, }, { name: "construction with MaxSecretVersions=0", config: Config{ MaxSecretVersions: 0, }, verify: func(t *testing.T, kv *KV) { // Document current behavior: 0 is accepted but may cause issues if kv.maxSecretVersions != 0 { t.Errorf("maxSecretVersions = %d, want 0", kv.maxSecretVersions) } // Note: This may cause all versions to be pruned immediately // Consider adding validation in New() if this is problematic }, }, { name: "construction with negative MaxSecretVersions", config: Config{ MaxSecretVersions: -5, }, verify: func(t *testing.T, kv *KV) { // Document current behavior: negative values are accepted if kv.maxSecretVersions != -5 { t.Errorf("maxSecretVersions = %d, want -5", kv.maxSecretVersions) } // Note: This may cause unexpected pruning behavior // Consider adding validation in New() if this is problematic }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := New(tt.config) tt.verify(t, kv) }) } } spike-sdk-go-0.16.4/kv/list.go000066400000000000000000000020641511163700700160240ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv // List returns a slice containing all paths stored in the key-value store. // The order of paths in the returned slice is not guaranteed to be stable // between calls. // // Note: List returns all paths regardless of whether their versions have been // deleted. A path is only removed from the store when all of its data is // explicitly removed, not when versions are soft-deleted. // // Returns: // - []string: A slice containing all paths present in the store // // Example: // // kv := New(Config{MaxSecretVersions: 10}) // kv.Put("app/config", map[string]string{"key": "value"}) // kv.Put("app/database", map[string]string{"host": "localhost"}) // // paths := kv.List() // // Returns: ["app/config", "app/database"] (order not guaranteed) func (kv *KV) List() []string { keys := make([]string, 0, len(kv.data)) for k := range kv.data { keys = append(keys, k) } return keys } spike-sdk-go-0.16.4/kv/list_test.go000066400000000000000000000022361511163700700170640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import "testing" func TestKV_List(t *testing.T) { tests := []struct { name string setup func() *KV want []string }{ { name: "empty_store", setup: func() *KV { return &KV{ data: make(map[string]*Value), } }, want: []string{}, }, { name: "non_empty_store", setup: func() *KV { return &KV{ data: map[string]*Value{ "test/path": { Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{ "key": "value", }, Version: 1, }, }, }, }, } }, want: []string{"test/path"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() got := kv.List() if len(got) != len(tt.want) { t.Errorf("got %v want %v", got, tt.want) } for i := range got { if got[i] != tt.want[i] { t.Errorf("got %v want %v", got, tt.want) } } }) } } spike-sdk-go-0.16.4/kv/put.go000066400000000000000000000066511511163700700156670ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "time" ) // Put stores a new version of key-value pairs at the specified path in the // store. It implements automatic versioning as a bounded cache with a // configurable maximum number of versions per path. // // When storing values: // - If the path doesn't exist, it creates new data with initial metadata // - Each put operation creates a new version with an incremented version // number // - Old versions are automatically pruned when they fall outside the version // window (CurrentVersion - MaxVersions) // - All versions exceeding MaxVersions are pruned in a single operation, // maintaining the most recent MaxVersions versions // - Timestamps are updated for both creation and modification times // // Version pruning behavior (bounded cache): // - Pruning occurs on each Put when versions exceed MaxVersions // - All versions older than (CurrentVersion - MaxVersions) are deleted // - Example: If CurrentVersion=15 and MaxVersions=10, versions 1-5 are // deleted, keeping versions 6-15 // - This ensures O(n) pruning where n is the number of excess versions, // providing predictable performance // // Parameters: // - path: The location where the data will be stored // - values: A map of key-value pairs to store at this path // // Example: // // kv := New(Config{MaxSecretVersions: 10}) // kv.Put("app/config", map[string]string{ // "api_key": "secret123", // "timeout": "30s", // }) // // Creates version 1 at path "app/config" // // kv.Put("app/config", map[string]string{ // "api_key": "newsecret456", // "timeout": "60s", // }) // // Creates version 2, version 1 is still available // // The function maintains metadata including: // - CreatedTime: When the data at this path was first created // - UpdatedTime: When the most recent version was added // - CurrentVersion: The latest version number // - OldestVersion: The oldest available version number after pruning // - MaxVersions: Maximum number of versions to keep (configurable at KV // creation) func (kv *KV) Put(path string, values map[string]string) { rightNow := time.Now() secret, exists := kv.data[path] if !exists { secret = &Value{ Versions: make(map[int]Version), Metadata: Metadata{ CreatedTime: rightNow, UpdatedTime: rightNow, MaxVersions: kv.maxSecretVersions, // Versions start at 1, so that passing 0 as the version will // default to the current version. CurrentVersion: 1, OldestVersion: 1, }, } kv.data[path] = secret } else { secret.Metadata.CurrentVersion++ } newVersion := secret.Metadata.CurrentVersion // Add a new version: secret.Versions[newVersion] = Version{ Data: values, CreatedTime: rightNow, Version: newVersion, } // Update metadata secret.Metadata.UpdatedTime = rightNow // Clean up the old versions if exceeding MaxVersions var deletedAny bool for version := range secret.Versions { if newVersion-version >= secret.Metadata.MaxVersions { delete(secret.Versions, version) deletedAny = true } } if deletedAny { oldestVersion := secret.Metadata.CurrentVersion for version := range secret.Versions { if version < oldestVersion { oldestVersion = version } } secret.Metadata.OldestVersion = oldestVersion } } spike-sdk-go-0.16.4/kv/put_test.go000066400000000000000000000051161511163700700167210ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "testing" ) func TestKV_Put(t *testing.T) { tests := []struct { name string setup func() *KV path string values map[string]string versions []int wantErr error }{ { setup: func() *KV { return &KV{ data: make(map[string]*Value), maxSecretVersions: 10, } }, name: "it creates a new secret with initial metadata if the path doesn't exist", path: "new/secret/path", versions: []int{1}, values: map[string]string{"key": "value"}, wantErr: nil, }, { name: "it creates a new version with an incremented version number", setup: func() *KV { kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10} kv.Put("existing/secret/path", map[string]string{"key": "value1"}) return kv }, path: "existing/secret/path", versions: []int{1, 2}, wantErr: nil, }, { name: "it automatically prunes old versions when exceeding MaxVersions", setup: func() *KV { kv := &KV{data: make(map[string]*Value), maxSecretVersions: 2} kv.Put("prune/old/versions", map[string]string{"key": "value1"}) kv.Put("prune/old/versions", map[string]string{"key": "value2"}) kv.Put("prune/old/versions", map[string]string{"key": "value3"}) return kv }, path: "prune/old/versions", versions: []int{4, 3}, values: map[string]string{ "key": "value4", }, wantErr: nil, }, { name: "it updates timestamps for both creation and modification times", setup: func() *KV { kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10} kv.Put("update/timestamps", map[string]string{"key": "value1"}) return kv }, versions: []int{1, 2}, path: "update/timestamps", values: map[string]string{"key": "value2"}, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() kv.Put(tt.path, tt.values) secret, exists := kv.data[tt.path] if !exists { t.Fatalf("expected secret to exist at path %q", tt.path) } if len(secret.Versions) != len(tt.versions) { t.Fatalf("expected %d versions, got %d", len(tt.versions), len(secret.Versions)) } for _, version := range tt.versions { if _, exists := secret.Versions[version]; !exists { t.Fatalf("expected version %d to exist", version) } } if tt.wantErr != nil { t.Fatalf("unexpected error: %v", tt.wantErr) } }) } } spike-sdk-go-0.16.4/kv/undelete.go000066400000000000000000000057021511163700700166600ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import sdkErrors "github.com/spiffe/spike-sdk-go/errors" // Undelete restores previously deleted versions of a secret at the specified // path. It sets the DeletedTime to nil for each specified version that exists. // // The function supports flexible version restoration with the following behavior: // - If versions is empty, restores only the current version // - If versions contains specific numbers, restores those versions // - Version 0 in the array represents the current version // - Non-existent versions are silently skipped without error // // This idempotent behavior is useful for batch operations where you want to // ensure certain versions are restored without failing if some don't exist. // // Parameters: // - path: The location of the secret in the store // - versions: Array of version numbers to restore (empty array restores // current version only, 0 in the array represents current version) // // Returns: // - []int: Array of version numbers that were actually modified (had their // DeletedTime changed from non-nil to nil). Already-restored versions are // not included in this list. // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrEntityNotFound: if the path doesn't exist // // Example: // // // Restore current version only // modified, err := kv.Undelete("secret/path", []int{}) // if err != nil { // log.Printf("Failed to undelete secret: %v", err) // } // log.Printf("Restored %d version(s): %v", len(modified), modified) // // // Restore specific versions // modified, err = kv.Undelete("secret/path", []int{1, 2, 3}) // if err != nil { // log.Printf("Failed to undelete versions: %v", err) // } // log.Printf("Actually restored: %v", modified) func (kv *KV) Undelete(path string, versions []int) ([]int, *sdkErrors.SDKError) { secret, exists := kv.data[path] if !exists { return nil, sdkErrors.ErrEntityNotFound } cv := secret.Metadata.CurrentVersion var modified []int // If no versions specified, mark the latest version as undeleted if len(versions) == 0 { if v, exists := secret.Versions[cv]; exists && v.DeletedTime != nil { v.DeletedTime = nil // Mark as undeleted. secret.Versions[cv] = v modified = append(modified, cv) } return modified, nil } // Undelete specific versions for _, version := range versions { if version == 0 { v, exists := secret.Versions[cv] if !exists || v.DeletedTime == nil { continue } v.DeletedTime = nil // Mark as undeleted. secret.Versions[cv] = v modified = append(modified, cv) continue } if v, exists := secret.Versions[version]; exists && v.DeletedTime != nil { v.DeletedTime = nil // Mark as undeleted. secret.Versions[version] = v modified = append(modified, version) } } return modified, nil } spike-sdk-go-0.16.4/kv/undelete_test.go000066400000000000000000000103771511163700700177230ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package kv import ( "errors" "testing" "time" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) func TestKV_Undelete(t *testing.T) { tests := []struct { name string setup func() *KV path string values map[string]string versions []int wantErr error }{ { name: "undelete latest version if no versions specified", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, Version: 1, DeletedTime: &time.Time{}, }, }, } return kv }, path: "test/path", versions: []int{}, wantErr: nil, }, { name: "undelete specific versions", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 2, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value1"}, Version: 1, DeletedTime: &time.Time{}, }, 2: { Data: map[string]string{"key": "value2"}, Version: 2, DeletedTime: &time.Time{}, }, }, } return kv }, path: "test/path", versions: []int{1, 2}, wantErr: nil, }, { name: "if secret does not exist", setup: func() *KV { return &KV{ data: make(map[string]*Value), maxSecretVersions: 10, } }, path: "path/undelete/notExist", versions: []int{1}, values: map[string]string{"key": "value"}, wantErr: sdkErrors.ErrEntityNotFound, }, { name: "skip non-existent versions", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, Version: 1, DeletedTime: &time.Time{}, }, }, } return kv }, path: "test/path", versions: []int{1, 2}, wantErr: nil, }, { name: "skip non-existent versions", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 2, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, Version: 1, DeletedTime: &time.Time{}, }, }, } return kv }, path: "test/path", versions: []int{0}, wantErr: nil, }, { name: "if version is 0 undelete current version", setup: func() *KV { kv := &KV{ data: make(map[string]*Value), } kv.data["test/path"] = &Value{ Metadata: Metadata{ CurrentVersion: 1, }, Versions: map[int]Version{ 1: { Data: map[string]string{"key": "value"}, Version: 1, DeletedTime: &time.Time{}, }, }, } return kv }, path: "test/path", values: map[string]string{"key": "value"}, versions: []int{0}, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kv := tt.setup() _, err := kv.Undelete(tt.path, tt.versions) // Handle nil case explicitly to avoid typed nil vs untyped nil issues if tt.wantErr == nil { if err != nil { t.Errorf("Undelete() error = %v, wantErr nil", err) return } } else if !errors.Is(err, tt.wantErr) { t.Errorf("Undelete() error = %v, wantErr %v", err, tt.wantErr) return } if err == nil { secret, exist := kv.data[tt.path] if !exist { t.Errorf("Secret should exist at path %s", tt.path) return } for _, version := range tt.versions { if version == 0 { version = secret.Metadata.CurrentVersion } if v, exist := secret.Versions[version]; exist { if v.DeletedTime != nil { t.Errorf("Version %d should not be deleted", version) } } } } }) } } spike-sdk-go-0.16.4/log/000077500000000000000000000000001511163700700146615ustar00rootroot00000000000000spike-sdk-go-0.16.4/log/fatal.go000066400000000000000000000030141511163700700162750ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package log import ( "fmt" "os" "strings" ) // Cannot import "env" here because of circular dependency. const stackTracesOnLogFatalEnvVar = "SPIKE_STACK_TRACES_ON_LOG_FATAL" // stackTracesOnLogFatalVal checks if stack traces should be enabled for fatal // log calls by reading the SPIKE_STACK_TRACES_ON_LOG_FATAL environment // variable. // // Returns: // - bool: true if the environment variable is set to "true" // (case-insensitive), // false otherwise or if the variable is empty/unset func stackTracesOnLogFatalVal() bool { s := os.Getenv(stackTracesOnLogFatalEnvVar) s = strings.ToLower(strings.TrimSpace(s)) if s == "" { return false } return s == "true" } // fatalExit terminates the program with exit code 1, or panics with a stack // trace if SPIKE_STACK_TRACES_ON_LOG_FATAL is enabled. This provides a way // to get detailed stack traces for debugging during development while using // clean exits in production. // // Parameters: // - fName: the name of the calling function for stack trace identification // - args: variadic arguments to include in the panic message if stack traces // are enabled func fatalExit(fName string, args []any) { if stackTracesOnLogFatalVal() { ss := make([]string, len(args)) for i, arg := range args { ss[i] = fmt.Sprint(arg) } panic(fName + " " + strings.Join(ss, ",")) } os.Exit(1) } spike-sdk-go-0.16.4/log/log.go000066400000000000000000000137051511163700700157770ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package log provides a lightweight thread-safe logging facility // using structured logging (slog) with JSON output format. It offers a // singleton logger instance with configurable log levels through environment // variables and convenience methods for fatal error logging. package log import ( "log/slog" "os" "strings" "sync" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) var logger *slog.Logger var loggerMutex sync.Mutex // Log returns a thread-safe singleton instance of slog.Logger configured for // JSON output. If the logger hasn't been initialized, it creates a new instance // with the log level specified by the environment. Further calls return the // same logger instance. // // By convention, when using the returned logger, the first argument (msg) // should be the function name (fName) from which the logging call is made. // // Returns: // - *slog.Logger: A thread-safe singleton logger instance func Log() *slog.Logger { loggerMutex.Lock() defer loggerMutex.Unlock() if logger != nil { return logger } opts := &slog.HandlerOptions{ Level: Level(), } handler := slog.NewJSONHandler(os.Stdout, opts) logger = slog.New(handler) return logger } // Debug logs a message at Debug level. // // Parameters: // - msg: The function name from which the call is made // - args: Key-value pairs to be logged as structured fields func Debug(msg string, args ...any) { Log().Debug(msg, args...) } // Info logs a message at Info level. // // Parameters: // - msg: The function name from which the call is made // - args: Key-value pairs to be logged as structured fields func Info(msg string, args ...any) { Log().Info(msg, args...) } // Warn logs a message at Warn level. // // Parameters: // - msg: The function name from which the call is made // - args: Key-value pairs to be logged as structured fields func Warn(msg string, args ...any) { Log().Warn(msg, args...) } // Error logs a message at Error level. // // Parameters: // - msg: The function name from which the call is made // - args: Key-value pairs to be logged as structured fields func Error(msg string, args ...any) { Log().Error(msg, args...) } // FatalLn logs a message at Fatal level with a line feed. // // By default, this function exits cleanly with status code 1 to avoid leaking // sensitive information through stack traces in production. To enable stack // traces for development and testing, set SPIKE_STACK_TRACES_ON_LOG_FATAL=true. // // Parameters: // - fName: The function name from which the call is made // - args: The values to be logged, which will be formatted and joined func FatalLn(fName string, args ...any) { Log().Error(fName, args...) fatalExit(fName, args) } // DebugErr logs an SDK error at Debug level. // // Parameters: // - fName: The function name from which the call is made // - err: An SDKError that will be logged with its message, code, and error // text as structured fields func DebugErr(fName string, err sdkErrors.SDKError) { Log().Debug( fName, "message", err.Msg, "code", err.Code, "err", err.Error(), ) } // InfoErr logs an SDK error at Info level. // // Parameters: // - fName: The function name from which the call is made // - err: An SDKError that will be logged with its message, code, and error // text as structured fields func InfoErr(fName string, err sdkErrors.SDKError) { Log().Info( fName, "message", err.Msg, "code", err.Code, "err", err.Error(), ) } // WarnErr logs an SDK error at Warn level. // // Parameters: // - fName: The function name from which the call is made // - err: An SDKError that will be logged with its message, code, and error // text as structured fields func WarnErr(fName string, err sdkErrors.SDKError) { Log().Warn( fName, "message", err.Msg, "code", err.Code, "err", err.Error(), ) } // ErrorErr logs an SDK error at Error level. // // Parameters: // - fName: The function name from which the call is made // - err: An SDKError that will be logged with its message, code, and error // text as structured fields func ErrorErr(fName string, err sdkErrors.SDKError) { Log().Error( fName, "message", err.Msg, "code", err.Code, "err", err.Error(), ) } // FatalErr logs an SDK error at Fatal level and exits the program. // // By default, this function exits cleanly with status code 1 to avoid leaking // sensitive information through stack traces in production. To enable stack // traces for development and testing, set SPIKE_STACK_TRACES_ON_LOG_FATAL=true. // // Parameters: // - fName: The function name from which the call is made // - err: An SDKError that will be logged with its message, code, and error // text as structured fields func FatalErr(fName string, err sdkErrors.SDKError) { FatalLn( fName, "message", err.Msg, "code", err.Code, "err", err.Error(), ) } // Cannot get from env.go because of circular dependency. const systemLogLevelEnvVar = "SPIKE_SYSTEM_LOG_LEVEL" // Level returns the logging level for the SPIKE components. // // It reads from the SPIKE_SYSTEM_LOG_LEVEL environment variable and // converts it to the corresponding slog.Level value. // // Returns: // - slog.Level: The configured log level. Valid values (case-insensitive) are: // - "DEBUG": returns slog.LevelDebug // - "INFO": returns slog.LevelInfo // - "WARN": returns slog.LevelWarn (default) // - "ERROR": returns slog.LevelError // // If the environment variable is not set or contains an invalid value, // it returns the default level slog.LevelWarn. func Level() slog.Level { level := os.Getenv(systemLogLevelEnvVar) level = strings.ToUpper(level) switch level { case "DEBUG": return slog.LevelDebug // -4 case "INFO": return slog.LevelInfo // 0 case "WARN": return slog.LevelWarn // 4 case "ERROR": return slog.LevelError // 8 default: return slog.LevelWarn // 4 } } spike-sdk-go-0.16.4/net/000077500000000000000000000000001511163700700146665ustar00rootroot00000000000000spike-sdk-go-0.16.4/net/doc.go000066400000000000000000000027341511163700700157700ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package net provides network utilities for secure mTLS communication using // SPIFFE X.509 certificates. // // The package includes functionality for: // - Creating mTLS HTTP clients and servers with SPIFFE authentication // - Predicate-based authorization for fine-grained access control // - HTTP POST operations with both JSON and streaming support // - Request and response body handling with proper resource cleanup // - Common HTTP error handling and status code mapping // // All network operations use mutual TLS authentication, where both client and // server verify each other's SPIFFE identities. Predicates can be used to // restrict which peer SPIFFE IDs are allowed to connect. // // Example usage: // // // Create an mTLS client that only connects to SPIKE Nexus servers // source, _ := workloadapi.NewX509Source(ctx) // client, err := net.CreateMTLSClientWithPredicate(source, predicate.AllowNexus) // // // Make a secure POST request // payload := []byte(`{"key": "value"}`) // response, err := net.Post(client, "https://nexus:8443/api/v1/secrets", payload) // // // Create an mTLS server with custom predicate // server, err := net.CreateMTLSServerWithPredicate(source, ":8443", // func(id string) bool { // return strings.HasPrefix(id, "spiffe://example.org/") // }) package net spike-sdk-go-0.16.4/net/header.go000066400000000000000000000005721511163700700164510ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\\ SPDX-License-Identifier: Apache-2.0 package net type ContentType string var ContentTypeJSON ContentType = "application/json" var ContentTypePlain ContentType = "text/plain" var ContentTypeOctetStream ContentType = "application/octet-stream" spike-sdk-go-0.16.4/net/net.go000066400000000000000000000351531511163700700160120ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "context" "fmt" "io" "net" "net/http" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/config/env" "github.com/spiffe/spike-sdk-go/log" "github.com/spiffe/spike-sdk-go/predicate" "github.com/spiffe/spike-sdk-go/spiffe" ) // RequestBody reads and returns the entire request body as a byte slice. // It reads all data from r.Body and ensures the body is properly closed // after reading, even if an error occurs during the read operation. // // Close errors are logged but not returned to the caller, as the primary // operation (reading the body data) has already completed. If reading fails, // the error is returned immediately. // // Parameters: // - r: HTTP request containing the body to read // // Returns: // - bod: byte slice containing the full request body data on success, nil on // error // - err: *sdkErrors.SDKError with ErrNetReadingRequestBody if reading fails, // nil on success (close errors are only logged) // // Example: // // body, err := RequestBody(req) // if err != nil { // log.Printf("Failed to read request body: %v", err) // return // } // // Process body data... func RequestBody(r *http.Request) (bod []byte, err *sdkErrors.SDKError) { const fName = "RequestBody" body, e := io.ReadAll(r.Body) if e != nil { failErr := sdkErrors.ErrNetReadingRequestBody.Wrap(e) return nil, failErr } defer func(b io.ReadCloser) { if b == nil { return } failErr := sdkErrors.ErrFSStreamCloseFailed log.WarnErr(fName, *failErr) }(r.Body) return body, err } // AuthorizerWithPredicate creates a TLS authorizer that validates SPIFFE IDs // using the provided predicate function. // // The authorizer checks each connecting peer's SPIFFE ID against the predicate. // If the predicate returns true, the connection is authorized. If false, the // connection is rejected with ErrAccessUnauthorized. // // Parameters: // - predicate: Function that takes a SPIFFE ID string and returns true to // allow the connection, false to reject it // // Returns: // - tlsconfig.Authorizer: A TLS authorizer that can be used with mTLS configs // // Example: // // // Allow only production namespace // authorizer := AuthorizerWithPredicate(func(id string) bool { // return strings.Contains(id, "/ns/production/") // }) func AuthorizerWithPredicate(predicate func(string) bool) tlsconfig.Authorizer { return tlsconfig.AdaptMatcher(func(id spiffeid.ID) error { if predicate(id.String()) { return nil } failErr := sdkErrors.ErrAccessUnauthorized failErr.Msg = fmt.Sprintf("unauthorized spiffe id: '%s'", id.String()) return failErr }) } // CreateMTLSServerWithPredicate creates an HTTP server configured for mutual // TLS (mTLS) authentication using SPIFFE X.509 certificates. It sets up the // server with a custom authorizer that validates client SPIFFE IDs against a // provided predicate function. // // Parameters: // - source: An X509Source that provides the server's identity credentials and // validates client certificates. Must not be nil. // - tlsPort: The network address and port for the server to listen on // (e.g., ":8443"). // - predicate: A function that takes a client SPIFFE ID string and returns // true if the client should be allowed access, false otherwise. // // Returns: // - *http.Server: A configured HTTP server ready to be started with TLS // enabled. // // The server uses the provided X509Source for both its own identity and for // validating client certificates. Client connections are only accepted if their // SPIFFE ID passes the provided predicate function. // // Note: Terminates the program via log.FatalErr if source is nil, as this // indicates a critical configuration error that should be caught during development. func CreateMTLSServerWithPredicate(source *workloadapi.X509Source, tlsPort string, predicate func(string) bool) *http.Server { const fName = "CreateMTLSServerWithPredicate" if source == nil { failErr := sdkErrors.ErrSPIFFENilX509Source failErr.Msg = "source cannot be nil" log.FatalErr(fName, *failErr) } authorizer := AuthorizerWithPredicate(predicate) tlsConfig := tlsconfig.MTLSServerConfig(source, source, authorizer) server := &http.Server{ Addr: tlsPort, TLSConfig: tlsConfig, ReadHeaderTimeout: env.HTTPServerReadHeaderTimeoutVal(), // ^ Timeout for reading request headers, // it helps prevent slowloris attacks } return server } // CreateMTLSServer creates an HTTP server configured for mutual TLS (mTLS) // authentication using SPIFFE X.509 certificates. // // WARNING: This function accepts ALL client SPIFFE IDs without validation. // For production use, consider using CreateMTLSServerWithPredicate to restrict // which clients can connect to this server for better security. // // Parameters: // - source: An X509Source that provides the server's identity credentials and // validates client certificates. Must not be nil. // - tlsPort: The network address and port for the server to listen on // (e.g., ":8443"). // // Returns: // - *http.Server: A configured HTTP server ready to be started with TLS // enabled. // // The server uses the provided X509Source for both its own identity and for // validating client certificates. Client connections are accepted from ANY // client with a valid SPIFFE certificate. // // Note: Terminates the program via log.FatalErr if source is nil, as this // indicates a critical configuration error that should be caught during development. func CreateMTLSServer(source *workloadapi.X509Source, tlsPort string) *http.Server { return CreateMTLSServerWithPredicate(source, tlsPort, predicate.AllowAll) } // CreateMTLSClientWithPredicate creates an HTTP client configured for // mutual TLS authentication using SPIFFE workload identities. // // Parameters: // - source: An X509Source that provides: // - The client's own identity certificate (presented to servers) // - Trusted roots for validating server certificates // - predicate: A function that validates SERVER (peer) SPIFFE IDs. // Returns true if the SERVER's ID should be trusted. // NOTE: This predicate checks the SERVER's identity, NOT the client's. // // Returns: // - *http.Client: A configured HTTP client that will use mTLS for all // connections // // The returned client will: // - Present its own client certificate from the X509Source to servers // - Validate server certificates using the same X509Source's trust bundle // - Only accept connections to servers whose SPIFFE IDs pass the predicate // // Example: // // // This predicate allows the client to connect only to servers with // // SPIFFE IDs in the "backend" service namespace // client := CreateMTLSClientWithPredicate(source, // func(serverID string) bool { // return strings.Contains(serverID, "/ns/backend/") // }) func CreateMTLSClientWithPredicate( source *workloadapi.X509Source, predicate predicate.Predicate, ) *http.Client { authorizer := AuthorizerWithPredicate(predicate) tlsConfig := tlsconfig.MTLSClientConfig(source, source, authorizer) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, IdleConnTimeout: env.HTTPClientIdleConnTimeoutVal(), MaxIdleConns: env.HTTPClientMaxIdleConnsVal(), MaxConnsPerHost: env.HTTPClientMaxConnsPerHostVal(), MaxIdleConnsPerHost: env.HTTPClientMaxIdleConnsPerHostVal(), DialContext: (&net.Dialer{ Timeout: env.HTTPClientDialerTimeoutVal(), KeepAlive: env.HTTPClientDialerKeepAliveVal(), }).DialContext, TLSHandshakeTimeout: env.HTTPClientTLSHandshakeTimeoutVal(), ResponseHeaderTimeout: env.HTTPClientResponseHeaderTimeoutVal(), ExpectContinueTimeout: env.HTTPClientExpectContinueTimeoutVal(), }, Timeout: env.HTTPClientTimeoutVal(), } return client } // CreateMTLSClient creates an HTTP client configured for mutual TLS // authentication using SPIFFE workload identities. // // WARNING: This function accepts ALL server SPIFFE IDs without validation. // For production use, consider using CreateMTLSClientWithPredicate to restrict // which servers this client will connect to for better security. // // Parameters: // - source: An X509Source that provides the client's identity certificates // and trusted roots // // Returns: // - *http.Client: A configured HTTP client that will use mTLS for all // connections // // The returned client will: // - Present client certificates from the provided X509Source // - Validate server certificates using the same X509Source // - Accept connections to ANY server with a valid SPIFFE certificate func CreateMTLSClient(source *workloadapi.X509Source) *http.Client { return CreateMTLSClientWithPredicate(source, predicate.AllowAll) } // CreateMTLSClientForNexus creates an HTTP client configured for mutual TLS // authentication with SPIKE Nexus using the provided X509Source. The client // is configured with a predicate that validates peer IDs against the trusted // Nexus root. Only peers that pass the spiffeid.IsNexus validation will be // accepted for connections. // // Parameters: // - source: An X509Source that provides the client's identity certificates // and trusted roots // // Returns: // - *http.Client: A configured HTTP client for connecting to SPIKE Nexus func CreateMTLSClientForNexus(source *workloadapi.X509Source) *http.Client { return CreateMTLSClientWithPredicate(source, predicate.AllowNexus) } // CreateMTLSClientForKeeper creates an HTTP client configured for mutual // TLS authentication using the provided X509Source. The client is configured // with a predicate that validates peer IDs against the trusted keeper root. // Only peers that pass the spiffeid.IsKeeper validation will be accepted for // connections. // // Parameters: // - source: An X509Source that provides the client's identity certificates // and trusted roots // // Returns: // - *http.Client: A configured HTTP client for connecting to SPIKE Keeper func CreateMTLSClientForKeeper(source *workloadapi.X509Source) *http.Client { return CreateMTLSClientWithPredicate(source, predicate.AllowKeeper) } // Source creates and returns a new SPIFFE X509Source for workload API // communication. It establishes a connection to the SPIFFE workload API using // the default endpoint socket with a configurable timeout to prevent indefinite // blocking on socket issues. // // The timeout can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT // environment variable (default: 30s). // // The function will terminate the program with exit code 1 if the source // creation fails or times out. // // Returns: // - *workloadapi.X509Source: A new X509Source for SPIFFE workload API // communication func Source() *workloadapi.X509Source { const fName = "Source" ctx, cancel := context.WithTimeout( context.Background(), env.SPIFFESourceTimeoutVal(), ) defer cancel() source, _, err := spiffe.Source(ctx, spiffe.EndpointSocket()) if err != nil { failErr := sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err) log.FatalErr(fName, *failErr) } return source } // ServeWithPredicate initializes and starts an HTTPS server using mTLS // authentication with SPIFFE X.509 certificates. It sets up the server routes // using the provided initialization function and listens for incoming // connections on the specified port. // // Parameters: // - source: An X509Source that provides the server's identity credentials and // validates client certificates. Must not be nil. // - initializeRoutes: A function that sets up the HTTP route handlers for the // server. This function is called before the server starts. // - predicate: a predicate function to pass to CreateMTLSServer. // - tlsPort: The network address and port for the server to listen to on // (e.g., ":8443"). // // Returns: // - *sdkErrors.SDKError: Returns nil if the server starts successfully, // otherwise returns one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrFSStreamOpenFailed: if the server fails to start or encounters an error // while running // // The function uses empty strings for the certificate and key file parameters // in ListenAndServeTLS as the certificates are provided by the X509Source. The // server's mTLS configuration is determined by the CreateMTLSServer function. func ServeWithPredicate(source *workloadapi.X509Source, initializeRoutes func(), predicate func(string) bool, tlsPort string) *sdkErrors.SDKError { if source == nil { failErr := sdkErrors.ErrSPIFFENilX509Source failErr.Msg = "got nil source while trying to serve" return failErr } initializeRoutes() server := CreateMTLSServerWithPredicate(source, tlsPort, predicate) if err := server.ListenAndServeTLS("", ""); err != nil { failErr := sdkErrors.ErrFSStreamOpenFailed.Wrap(err) failErr.Msg = "failed to listen and serve" return failErr } return nil } // Serve initializes and starts an HTTPS server using mTLS // authentication with SPIFFE X.509 certificates. It sets up the server routes // using the provided initialization function and listens for incoming // connections on the specified port. // // WARNING: This function accepts ALL client SPIFFE IDs without validation. // For production use, consider using ServeWithPredicate to restrict // which clients can connect to this server for better security. // // Parameters: // - source: An X509Source that provides the server's identity credentials and // validates client certificates. Must not be nil. // - initializeRoutes: A function that sets up the HTTP route handlers for the // server. This function is called before the server starts. // - tlsPort: The network address and port for the server to listen on // (e.g., ":8443"). // // Returns: // - *sdkErrors.SDKError: Returns nil if the server starts successfully, // otherwise returns one of the following errors: // - ErrSPIFFENilX509Source: if source is nil // - ErrFSStreamOpenFailed: if the server fails to start or encounters an error // while running // // The function uses empty strings for the certificate and key file parameters // in ListenAndServeTLS as the certificates are provided by the X509Source. The // server's mTLS configuration is determined by the CreateMTLSServer function. func Serve( source *workloadapi.X509Source, initializeRoutes func(), tlsPort string) *sdkErrors.SDKError { return ServeWithPredicate( source, initializeRoutes, predicate.AllowAll, tlsPort, ) } spike-sdk-go-0.16.4/net/net_test.go000066400000000000000000000305231511163700700170450ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestContentTypeConstants tests that content type constants are defined correctly func TestContentTypeConstants(t *testing.T) { assert.Equal(t, ContentType("application/json"), ContentTypeJSON) assert.Equal(t, ContentType("text/plain"), ContentTypePlain) assert.Equal(t, ContentType("application/octet-stream"), ContentTypeOctetStream) } // TestRequestBody_Success tests successful request body reading func TestRequestBody_Success(t *testing.T) { testData := []byte("test request body") req := httptest.NewRequest("POST", "/test", bytes.NewReader(testData)) body, err := RequestBody(req) assert.Nil(t, err) assert.Equal(t, testData, body) } // TestRequestBody_EmptyBody tests reading an empty request body func TestRequestBody_EmptyBody(t *testing.T) { req := httptest.NewRequest("POST", "/test", bytes.NewReader([]byte{})) body, err := RequestBody(req) assert.Nil(t, err) assert.Equal(t, []byte{}, body) } // TestRequestBody_LargeBody tests reading a large request body func TestRequestBody_LargeBody(t *testing.T) { // Create 1MB of test data largeData := make([]byte, 1024*1024) for i := range largeData { largeData[i] = byte(i % 256) } req := httptest.NewRequest("POST", "/test", bytes.NewReader(largeData)) body, err := RequestBody(req) assert.Nil(t, err) assert.Equal(t, largeData, body) assert.Equal(t, 1024*1024, len(body)) } // TestBody_Success tests successful response body reading func TestBody_Success(t *testing.T) { testData := "test response body" resp := &http.Response{ Body: io.NopCloser(strings.NewReader(testData)), } bodyBytes, err := body(resp) assert.Nil(t, err) assert.Equal(t, []byte(testData), bodyBytes) } // TestBody_EmptyResponse tests reading an empty response body func TestBody_EmptyResponse(t *testing.T) { resp := &http.Response{ Body: io.NopCloser(strings.NewReader("")), } bodyBytes, err := body(resp) assert.Nil(t, err) assert.Equal(t, []byte{}, bodyBytes) } // TestBody_JSONResponse tests reading a JSON response body func TestBody_JSONResponse(t *testing.T) { jsonData := `{"key":"value","number":42}` resp := &http.Response{ Body: io.NopCloser(strings.NewReader(jsonData)), } bodyBytes, err := body(resp) assert.Nil(t, err) assert.Equal(t, []byte(jsonData), bodyBytes) // Verify it's valid JSON var result map[string]interface{} unmarshalErr := json.Unmarshal(bodyBytes, &result) assert.NoError(t, unmarshalErr) assert.Equal(t, "value", result["key"]) assert.Equal(t, float64(42), result["number"]) } // Mock response type for testing PostAndUnmarshal type mockResponse struct { Data string `json:"data"` Err sdkErrors.ErrorCode `json:"err,omitempty"` } func (r mockResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // TestAuthorizerWithPredicate_Allow tests authorizer allowing connections func TestAuthorizerWithPredicate_Allow(t *testing.T) { // Create a predicate that allows all IDs containing "allowed" predicate := func(id string) bool { return strings.Contains(id, "allowed") } authorizer := AuthorizerWithPredicate(predicate) assert.NotNil(t, authorizer) } // TestAuthorizerWithPredicate_Deny tests authorizer denying connections func TestAuthorizerWithPredicate_Deny(t *testing.T) { // Create a predicate that denies all IDs predicate := func(_ string) bool { return false } authorizer := AuthorizerWithPredicate(predicate) assert.NotNil(t, authorizer) } // TestAuthorizerWithPredicate_ComplexLogic tests authorizer with complex predicate func TestAuthorizerWithPredicate_ComplexLogic(t *testing.T) { // Create a predicate with multiple conditions predicate := func(id string) bool { return strings.HasPrefix(id, "spiffe://") && strings.Contains(id, "/service/") && !strings.Contains(id, "/forbidden/") } authorizer := AuthorizerWithPredicate(predicate) assert.NotNil(t, authorizer) } // TestCreateMTLSServer_NilSource tests server creation with nil source func TestCreateMTLSServer_NilSource(t *testing.T) { // Enable panic on fatal to test fatal error behavior t.Setenv("SPIKE_STACK_TRACES_ON_LOG_FATAL", "true") defer func() { r := recover() require.NotNil(t, r, "Expected panic due to nil source") panicMsg := fmt.Sprint(r) assert.Contains(t, panicMsg, "CreateMTLSServerWithPredicate") }() // This should panic because source is nil CreateMTLSServer(nil, ":8443") t.Fatal("Should have panicked due to nil source") } // TestCreateMTLSServerWithPredicate_NilSource tests server creation with predicate and nil source func TestCreateMTLSServerWithPredicate_NilSource(t *testing.T) { // Enable panic on fatal to test fatal error behavior t.Setenv("SPIKE_STACK_TRACES_ON_LOG_FATAL", "true") defer func() { r := recover() require.NotNil(t, r, "Expected panic due to nil source") panicMsg := fmt.Sprint(r) assert.Contains(t, panicMsg, "CreateMTLSServerWithPredicate") }() predicate := func(_ string) bool { return true } // This should panic because source is nil CreateMTLSServerWithPredicate(nil, ":8443", predicate) t.Fatal("Should have panicked due to nil source") } // TestCreateMTLSClient_NilSource tests client creation with nil source func TestCreateMTLSClient_NilSource(t *testing.T) { // This test documents behavior - CreateMTLSClient does not validate nil source // The validation happens at connection time, not creation time client := CreateMTLSClient(nil) assert.NotNil(t, client) assert.NotNil(t, client.Transport) } // TestCreateMTLSClientWithPredicate_NilSource tests client creation with predicate and nil source func TestCreateMTLSClientWithPredicate_NilSource(t *testing.T) { predicate := func(_ string) bool { return true } client := CreateMTLSClientWithPredicate(nil, predicate) assert.NotNil(t, client) assert.NotNil(t, client.Transport) } // TestCreateMTLSClientForNexus_NilSource tests Nexus client creation func TestCreateMTLSClientForNexus_NilSource(t *testing.T) { client := CreateMTLSClientForNexus(nil) assert.NotNil(t, client) assert.NotNil(t, client.Transport) } // TestCreateMTLSClientForKeeper_NilSource tests Keeper client creation func TestCreateMTLSClientForKeeper_NilSource(t *testing.T) { client := CreateMTLSClientForKeeper(nil) assert.NotNil(t, client) assert.NotNil(t, client.Transport) } // TestServeWithPredicate_NilSource tests serve function with nil source func TestServeWithPredicate_NilSource(t *testing.T) { initializeRoutes := func() {} predicate := func(_ string) bool { return true } err := ServeWithPredicate(nil, initializeRoutes, predicate, ":8443") assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestServe_NilSource tests serve function with nil source func TestServe_NilSource(t *testing.T) { initializeRoutes := func() {} err := Serve(nil, initializeRoutes, ":8443") assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrSPIFFENilX509Source)) } // TestPost_ClientConfiguration tests that Post handles various HTTP responses func TestPost_ClientConfiguration(t *testing.T) { tests := []struct { name string statusCode int responseBody string expectedError *sdkErrors.SDKError shouldHaveBody bool }{ { name: "Success_200", statusCode: http.StatusOK, responseBody: `{"success":true}`, expectedError: nil, shouldHaveBody: true, }, { name: "NotFound_404", statusCode: http.StatusNotFound, responseBody: `{}`, expectedError: sdkErrors.ErrAPINotFound, shouldHaveBody: false, }, { name: "Unauthorized_401", statusCode: http.StatusUnauthorized, responseBody: `{}`, expectedError: sdkErrors.ErrAccessUnauthorized, shouldHaveBody: false, }, { name: "BadRequest_400", statusCode: http.StatusBadRequest, responseBody: `{}`, expectedError: sdkErrors.ErrAPIBadRequest, shouldHaveBody: false, }, { name: "ServiceUnavailable_503", statusCode: http.StatusServiceUnavailable, responseBody: `{}`, expectedError: sdkErrors.ErrStateNotReady, shouldHaveBody: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(tt.statusCode) _, _ = w.Write([]byte(tt.responseBody)) })) defer server.Close() client := server.Client() bodyBytes, err := Post(client, server.URL, []byte(`{"test":"data"}`)) if tt.expectedError != nil { assert.NotNil(t, err) assert.True(t, err.Is(tt.expectedError)) } else { assert.Nil(t, err) } if tt.shouldHaveBody { assert.NotEmpty(t, bodyBytes) } }) } } // TestStreamPost_Success tests successful streaming POST func TestStreamPost_Success(t *testing.T) { testData := []byte("streaming test data") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify content type assert.Equal(t, "application/octet-stream", r.Header.Get("Content-Type")) // Read and echo the request body body, _ := io.ReadAll(r.Body) w.WriteHeader(http.StatusOK) _, _ = w.Write(body) })) defer server.Close() client := server.Client() reader := bytes.NewReader(testData) responseBody, err := StreamPost(client, server.URL, reader) // Note: The implementation closes the response body in a defer, // so the returned io.ReadCloser may not be readable require.Nil(t, err) require.NotNil(t, responseBody) } // TestStreamPostWithContentType_CustomContentType tests streaming POST with custom content type func TestStreamPostWithContentType_CustomContentType(t *testing.T) { testData := []byte(`{"streaming":"json"}`) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify content type assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"response":"ok"}`)) })) defer server.Close() client := server.Client() reader := bytes.NewReader(testData) responseBody, err := StreamPostWithContentType( client, server.URL, reader, ContentTypeJSON, ) require.Nil(t, err) require.NotNil(t, responseBody) responseBody.Close() } // TestStreamPost_NotFound tests streaming POST with 404 response func TestStreamPost_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() client := server.Client() reader := bytes.NewReader([]byte("test")) responseBody, err := StreamPost(client, server.URL, reader) assert.Nil(t, responseBody) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrAPINotFound)) } // TestResponseWithError_Interface tests the ResponseWithError interface func TestResponseWithError_Interface(t *testing.T) { // Test that mockResponse implements ResponseWithError var _ ResponseWithError = mockResponse{} var _ ResponseWithError = &mockResponse{} resp := mockResponse{ Data: "test", Err: "test_error", } assert.Equal(t, sdkErrors.ErrorCode("test_error"), resp.ErrorCode()) } // TestResponseWithError_EmptyErrorCode tests response with no error code func TestResponseWithError_EmptyErrorCode(t *testing.T) { resp := mockResponse{ Data: "test", Err: "", } assert.Equal(t, sdkErrors.ErrorCode(""), resp.ErrorCode()) } // TestPost_RequestCreationFailure tests Post with invalid URL func TestPost_RequestCreationFailure(t *testing.T) { client := &http.Client{} // Use an invalid URL that will cause request creation to fail invalidURL := string([]byte{0x7f}) // Invalid UTF-8 _, err := Post(client, invalidURL, []byte(`{}`)) assert.NotNil(t, err) } // TestContentTypeString tests content type string conversion func TestContentTypeString(t *testing.T) { tests := []struct { contentType ContentType expected string }{ {ContentTypeJSON, "application/json"}, {ContentTypePlain, "text/plain"}, {ContentTypeOctetStream, "application/octet-stream"}, } for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { assert.Equal(t, tt.expected, string(tt.contentType)) }) } } spike-sdk-go-0.16.4/net/post.go000066400000000000000000000063401511163700700162050ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "bytes" "io" "net/http" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // Post performs an HTTP POST request with a JSON payload and returns the // response body. It handles the common cases of connection errors, non-200 // status codes, and proper response body handling. // // Parameters: // - client: An *http.Client used to make the request, typically // configured with TLS settings. // - path: The URL path to send the POST request to. // - mr: A byte slice containing the marshaled JSON request body. // // Returns: // - []byte: The response body if the request is successful // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrAPIBadRequest: if request creation fails or server returns 400 // - ErrNetPeerConnection: if connection to peer fails or unexpected status // code // - ErrAPINotFound: if server returns 404 // - ErrAccessUnauthorized: if server returns 401 // - ErrStateNotReady: if server returns 503 // - ErrNetReadingResponseBody: if reading response body fails // // The function ensures proper cleanup by always attempting to close the // response body, even if an error occurs during reading. // // Example: // // client := &http.Client{} // data := []byte(`{"key": "value"}`) // response, err := Post(client, "https://api.example.com/endpoint", data) // if err != nil { // log.Fatalf("failed to post: %v", err) // } func Post( client *http.Client, path string, mr []byte, ) ([]byte, *sdkErrors.SDKError) { const fName = "Post" // Create the request while preserving the mTLS client req, err := http.NewRequest("POST", path, bytes.NewBuffer(mr)) if err != nil { failErr := sdkErrors.ErrAPIBadRequest.Wrap(err) failErr.Msg = "failed to create request" return nil, failErr } // Set headers req.Header.Set("Content-Type", "application/json") // Use the existing mTLS client to make the request //nolint:bodyclose // Response body is properly closed in defer block r, err := client.Do(req) if err != nil { failErr := sdkErrors.ErrNetPeerConnection.Wrap(err) return []byte{}, failErr } defer func(b io.ReadCloser) { if b == nil { return } err := b.Close() if err != nil { failErr := sdkErrors.ErrFSStreamCloseFailed.Wrap(err) failErr.Msg = "failed to close response body" log.WarnErr(fName, *failErr) } }(r.Body) if r.StatusCode != http.StatusOK { if r.StatusCode == http.StatusNotFound { return []byte{}, sdkErrors.ErrAPINotFound } if r.StatusCode == http.StatusUnauthorized { return []byte{}, sdkErrors.ErrAccessUnauthorized } if r.StatusCode == http.StatusBadRequest { return []byte{}, sdkErrors.ErrAPIBadRequest } // SPIKE Nexus is likely not initialized or in bad shape: if r.StatusCode == http.StatusServiceUnavailable { return []byte{}, sdkErrors.ErrStateNotReady } failErr := sdkErrors.ErrNetPeerConnection failErr.Msg = "unexpected status code from peer" return []byte{}, failErr } b, sdkErr := body(r) if sdkErr != nil { return nil, sdkErr } return b, nil } spike-sdk-go-0.16.4/net/request.go000066400000000000000000000052221511163700700167060ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "encoding/json" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // ResponseWithError is an interface for response types that include an error // code field. // This allows generic error handling across different API response types. type ResponseWithError interface { ErrorCode() sdkErrors.ErrorCode } // PostAndUnmarshal performs a complete request/response cycle for SPIKE Nexus // API calls. It handles client creation, request posting, response // unmarshaling, and error checking. // // Type parameter T must be a response type that implements ResponseWithError. // // Parameters: // - source: X509Source for establishing mTLS connection to SPIKE Nexus // - urlPath: The URL path to send the POST request to // - requestBody: Marshaled JSON request body // // Returns: // - (*T, nil) containing the unmarshaled response if successful // - (nil, *sdkErrors.SDKError) if an error occurs: // - Errors from Post(): including ErrAPINotFound, ErrAccessUnauthorized, etc. // - ErrDataUnmarshalFailure: if response parsing fails // - Error from FromCode(): if the response contains an error code // // Note: Callers should check for specific errors and handle them as needed: // // response, err := net.PostAndUnmarshal[MyResponse](source, url, body) // if err != nil { // if err.Is(sdkErrors.ErrAPINotFound) { // // Handle not found case (e.g., return empty slice for lists) // return &[]MyType{}, nil // } // return nil, err // } // // Example: // // type MyResponse struct { // Data string `json:"data"` // Err sdkErrors.ErrorCode `json:"err,omitempty"` // } // // func (r *MyResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err } // // response, err := net.PostAndUnmarshal[MyResponse]( // source, "https://api.example.com/endpoint", requestBody) func PostAndUnmarshal[T ResponseWithError]( source *workloadapi.X509Source, urlPath string, requestBody []byte, ) (*T, *sdkErrors.SDKError) { client := CreateMTLSClientForNexus(source) body, err := Post(client, urlPath, requestBody) if err != nil { return nil, err } var response T if unmarshalErr := json.Unmarshal(body, &response); unmarshalErr != nil { failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr) failErr.Msg = "problem parsing response body" return nil, failErr } if errCode := response.ErrorCode(); errCode != "" { return nil, sdkErrors.FromCode(errCode) } return &response, nil } spike-sdk-go-0.16.4/net/response.go000066400000000000000000000024671511163700700170640ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "io" "net/http" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // body reads and returns the entire response body from an HTTP response. // The response body is read completely and returned as a byte slice. // // This is an internal helper function used by the net package to process // HTTP responses. // // Parameters: // - r: The HTTP response to read from // // Returns: // - []byte: The complete response body, nil on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrNetReadingResponseBody: if reading the response body fails // // Example: // // resp, err := http.Get(url) // if err != nil { // return nil, err // } // defer resp.Body.Close() // // bodyBytes, sdkErr := body(resp) // if sdkErr != nil { // log.Printf("Failed to read response body: %v", sdkErr) // return nil, sdkErr // } func body(r *http.Response) ([]byte, *sdkErrors.SDKError) { bodyBytes, err := io.ReadAll(r.Body) if err != nil { failErr := sdkErrors.ErrNetReadingResponseBody.Wrap(err) failErr.Msg = "failed to read HTTP response body" return nil, failErr } return bodyBytes, nil } spike-sdk-go-0.16.4/net/stream.go000066400000000000000000000105541511163700700165150ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\ Copyright 2024-present SPIKE contributors. // \\\\\\ SPDX-License-Identifier: Apache-2.0 package net import ( "io" "net/http" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // StreamPostWithContentType performs an HTTP POST request with streaming data // and a custom content type, returning the response body as a stream. // // This function is designed for streaming large amounts of data without loading // the entire payload into memory. // // Resource Management: On success, returns an open io.ReadCloser that the caller // MUST close (typically with defer). On error, any response body is automatically // closed by this function and nil is returned, following the canonical Go pattern // of returning (zero-value, error) on failures. // // Parameters: // - client *http.Client: The HTTP client to use for the request // - path string: The URL path to POST to // - body io.Reader: The request body data stream // - contentType ContentType: The MIME type of the request body // (e.g., ContentTypeJSON, ContentTypeTextPlain, ContentTypeOctetStream) // // Returns: // - io.ReadCloser: The response body stream on success (must be closed by caller), // nil on error (already closed by this function) // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrAPINotFound (404): Resource not found // - ErrAccessUnauthorized (401): Authentication required // - ErrAPIBadRequest (400): Invalid request // - ErrStateNotReady (503): Service unavailable // - Generic error for other non-200 status codes // // Example: // // data := strings.NewReader("large data payload") // response, err := StreamPostWithContentType(client, // "/api/upload", data, "text/plain") // if err != nil { // return err // } // defer response.Close() // // Process streaming response... func StreamPostWithContentType( client *http.Client, path string, body io.Reader, contentType ContentType, ) (io.ReadCloser, *sdkErrors.SDKError) { const fName = "StreamPostWithContentType" req, err := http.NewRequest("POST", path, body) if err != nil { failErr := sdkErrors.ErrAPIPostFailed.Wrap(err) failErr.Msg = "failed to create request" return nil, failErr } req.Header.Set("Content-Type", string(contentType)) r, err := client.Do(req) if err != nil { failErr := sdkErrors.ErrNetPeerConnection.Wrap(err) return nil, failErr } if r.StatusCode != http.StatusOK { // Close body on error paths before returning if r.Body != nil { closeErr := r.Body.Close() if closeErr != nil { failErr := sdkErrors.ErrFSStreamCloseFailed failErr.Msg = "failed to close response body on error path" log.WarnErr(fName, *failErr) } } switch r.StatusCode { case http.StatusNotFound: return nil, sdkErrors.ErrAPINotFound case http.StatusUnauthorized: return nil, sdkErrors.ErrAccessUnauthorized case http.StatusBadRequest: return nil, sdkErrors.ErrAPIBadRequest case http.StatusServiceUnavailable: return nil, sdkErrors.ErrStateNotReady default: failErr := sdkErrors.ErrNetPeerConnection return nil, failErr } } // Success: return open body for caller to close return r.Body, nil } // StreamPost is a convenience wrapper for StreamPostWithContentType that uses // the default content type ContentTypeOctetStream ("application/octet-stream"). // // This function is ideal for posting binary data or when the specific content // type doesn't matter. The caller is responsible for closing the returned // io.ReadCloser. // // Parameters: // - client *http.Client: The HTTP client to use for the request // - path string: The URL path to POST to // - body io.Reader: The request body data stream // // Returns: // - io.ReadCloser: The response body stream if successful // (must be closed by caller) // - *sdkErrors.SDKError: nil on success, or a well-known error // (see StreamPostWithContentType) // // Example: // // binaryData := bytes.NewReader(fileBytes) // response, err := StreamPost(client, "/api/upload", binaryData) // if err != nil { // return err // } // defer response.Close() // // Process response... func StreamPost( client *http.Client, path string, body io.Reader, ) (io.ReadCloser, *sdkErrors.SDKError) { return StreamPostWithContentType( client, path, body, ContentTypeOctetStream, ) } spike-sdk-go-0.16.4/predicate/000077500000000000000000000000001511163700700160405ustar00rootroot00000000000000spike-sdk-go-0.16.4/predicate/predicate.go000066400000000000000000000123411511163700700203300ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package predicate provides SPIFFE ID validation predicates for SPIKE API // access control. // // This package defines predicate functions that can be used to validate // SPIFFE IDs in API calls, enabling fine-grained access control based on // workload identity. // Predicates are used by API methods to restrict access to specific types of // workloads (e.g., only SPIKE Pilot instances). package predicate import ( "github.com/spiffe/spike-sdk-go/spiffeid" ) // Predicate is a function type that validates a SPIFFE ID string. // It returns true if the SPIFFE ID should be allowed access, false otherwise. // // Predicates are used throughout the SPIKE API to implement access control // policies based on workload identity. They are typically passed to API methods // to restrict which workloads can perform specific operations. // // Example usage: // // // Create a predicate that only allows pilot workloads // pilotPredicate := AllowPilot("example.org") // // // Use in an API call // policy, err := acl.GetPolicy(source, policyID, pilotPredicate) type Predicate func(string) bool // AllowAll is a predicate that accepts any SPIFFE ID. // This effectively disables access control and should be used with caution. // It's typically used when policy-based access control is handled at a // higher level. // // Example usage: // // // Allow any workload to access the API // secret, err := secret.Get(source, path, version, AllowAll) var AllowAll = Predicate(func(_ string) bool { return true }) // DenyAll is a predicate that rejects all SPIFFE IDs. // This can be used to temporarily disable access or as a default restrictive // policy. // // Example usage: // // // Deny all access during maintenance // policy, err := acl.GetPolicy(source, policyID, DenyAll) var DenyAll = Predicate(func(_ string) bool { return false }) // AllowNexus is a predicate that only allows SPIKE Nexus workloads. // It validates whether a given SPIFFE ID matches the SPIKE Nexus identity // pattern for the configured trust domains. // // This is used to restrict API access to only SPIKE Nexus instances, providing // an additional layer of security for sensitive operations that should only // be performed by the data plane storage component. // // The predicate uses trust domains configured via environment variables. // // Example usage: // // // Use predicate for nexus-only access // policy, err := acl.GetPolicy(source, policyID, AllowNexus) // secret, err := secret.Get(source, secretPath, version, AllowNexus) // // The returned predicate will accept SPIFFE IDs matching: // - "spiffe://example.org/spike/nexus" // - "spiffe://example.org/spike/nexus/instance-1" // - "spiffe://dev.example.org/spike/nexus" // - etc. // // based on the trust domains configured in the environment. var AllowNexus = Predicate( func(SPIFFEID string) bool { return spiffeid.IsNexus(SPIFFEID) }, ) // AllowKeeper is a predicate that only allows SPIKE Keeper workloads. // It validates whether a given SPIFFE ID matches the SPIKE Keeper identity // pattern for the configured trust domains. // // This is used to restrict API access to only SPIKE Keeper instances, providing // an additional layer of security for operations that should only be performed // by the key management component. // // The predicate uses trust domains configured via environment variables. // // Example usage: // // // Use predicate for keeper-only access // policy, err := acl.GetPolicy(source, policyID, AllowKeeper) // secret, err := secret.Get(source, secretPath, version, AllowKeeper) // // The predicate will accept SPIFFE IDs matching: // - "spiffe://example.org/spike/keeper" // - "spiffe://example.org/spike/keeper/instance-1" // - "spiffe://dev.example.org/spike/keeper" // - etc. // // based on the trust domains configured in the environment. var AllowKeeper = Predicate( func(SPIFFEID string) bool { return spiffeid.IsKeeper(SPIFFEID) }, ) // AllowKeeperPeer is a predicate function that validates whether a peer // SPIFFE ID is authorized to communicate with SPIKE Keeper instances. // // For security reasons, only SPIKE Nexus and SPIKE Bootstrap components // are allowed to communicate with SPIKE Keeper. This function enforces // this security policy by checking if the peer SPIFFE ID matches either // the Nexus or Bootstrap identity patterns. // // Parameters: // - peerSpiffeId: The SPIFFE ID string of the peer attempting to connect // // Returns: // - bool: true if the peer is authorized (Nexus or Bootstrap), // false otherwise // // Example usage: // // // Use in server configuration to restrict Keeper access // if AllowKeeperPeer(clientSpiffeId) { // // Allow connection to Keeper // } else { // // Deny connection // } // // The function will return true for SPIFFE IDs matching: // - SPIKE Nexus: "spiffe://example.org/spike/nexus" // - SPIKE Bootstrap: "spiffe://example.org/spike/bootstrap" // - Extended variants with additional path segments var AllowKeeperPeer = func(peerSpiffeId string) bool { // Security: Only SPIKE Nexus and SPIKE Bootstrap // can talk to SPIKE Keepers. return spiffeid.PeerCanTalkToKeeper(peerSpiffeId) } spike-sdk-go-0.16.4/predicate/predicate_test.go000066400000000000000000000231221511163700700213660ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package predicate import ( "os" "testing" "github.com/stretchr/testify/assert" ) // TestAllowAll_ReturnsTrue tests that AllowAll accepts any SPIFFE ID func TestAllowAll_ReturnsTrue(t *testing.T) { tests := []struct { name string spiffeID string }{ {"ValidSPIFFEID", "spiffe://example.org/service"}, {"Empty", ""}, {"NexusSPIFFEID", "spiffe://example.org/spike/nexus"}, {"KeeperSPIFFEID", "spiffe://example.org/spike/keeper"}, {"RandomString", "not-a-spiffe-id"}, {"WithSpecialChars", "spiffe://example.org/service@#$"}, {"VeryLong", "spiffe://example.org/" + string(make([]byte, 1000))}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowAll(tt.spiffeID) assert.True(t, result, "AllowAll should return true for: %s", tt.spiffeID) }) } } // TestDenyAll_ReturnsFalse tests that DenyAll rejects all SPIFFE IDs func TestDenyAll_ReturnsFalse(t *testing.T) { tests := []struct { name string spiffeID string }{ {"ValidSPIFFEID", "spiffe://example.org/service"}, {"Empty", ""}, {"NexusSPIFFEID", "spiffe://example.org/spike/nexus"}, {"KeeperSPIFFEID", "spiffe://example.org/spike/keeper"}, {"RandomString", "not-a-spiffe-id"}, {"WithSpecialChars", "spiffe://example.org/service@#$"}, {"VeryLong", "spiffe://example.org/" + string(make([]byte, 1000))}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DenyAll(tt.spiffeID) assert.False(t, result, "DenyAll should return false for: %s", tt.spiffeID) }) } } // TestAllowNexus_ValidNexusID tests that AllowNexus accepts valid Nexus SPIFFE IDs func TestAllowNexus_ValidNexusID(t *testing.T) { // Set up environment variable for Nexus trust root os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") tests := []struct { name string spiffeID string }{ {"ExactMatch", "spiffe://example.org/spike/nexus"}, {"WithInstance", "spiffe://example.org/spike/nexus/instance-1"}, {"WithPath", "spiffe://example.org/spike/nexus/path/to/service"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowNexus(tt.spiffeID) assert.True(t, result, "AllowNexus should return true for: %s", tt.spiffeID) }) } } // TestAllowNexus_InvalidNexusID tests that AllowNexus rejects non-Nexus SPIFFE IDs func TestAllowNexus_InvalidNexusID(t *testing.T) { // Set up environment variable for Nexus trust root os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") tests := []struct { name string spiffeID string }{ {"Empty", ""}, {"KeeperSPIFFEID", "spiffe://example.org/spike/keeper"}, {"PilotSPIFFEID", "spiffe://example.org/spike/pilot"}, {"RegularService", "spiffe://example.org/service"}, {"WrongDomain", "spiffe://other.org/spike/nexus"}, {"PartialMatch", "spiffe://example.org/spike/nexu"}, {"NotSPIFFEID", "not-a-spiffe-id"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowNexus(tt.spiffeID) assert.False(t, result, "AllowNexus should return false for: %s", tt.spiffeID) }) } } // TestAllowKeeper_ValidKeeperID tests that AllowKeeper accepts valid Keeper SPIFFE IDs func TestAllowKeeper_ValidKeeperID(t *testing.T) { // Set up environment variable for Keeper trust root os.Setenv("SPIKE_TRUST_ROOT_KEEPER", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_KEEPER") tests := []struct { name string spiffeID string }{ {"ExactMatch", "spiffe://example.org/spike/keeper"}, {"WithInstance", "spiffe://example.org/spike/keeper/instance-1"}, {"WithPath", "spiffe://example.org/spike/keeper/path/to/service"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowKeeper(tt.spiffeID) assert.True(t, result, "AllowKeeper should return true for: %s", tt.spiffeID) }) } } // TestAllowKeeper_InvalidKeeperID tests that AllowKeeper rejects non-Keeper SPIFFE IDs func TestAllowKeeper_InvalidKeeperID(t *testing.T) { // Set up environment variable for Keeper trust root os.Setenv("SPIKE_TRUST_ROOT_KEEPER", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_KEEPER") tests := []struct { name string spiffeID string }{ {"Empty", ""}, {"NexusSPIFFEID", "spiffe://example.org/spike/nexus"}, {"PilotSPIFFEID", "spiffe://example.org/spike/pilot"}, {"RegularService", "spiffe://example.org/service"}, {"WrongDomain", "spiffe://other.org/spike/keeper"}, {"PartialMatch", "spiffe://example.org/spike/keepe"}, {"NotSPIFFEID", "not-a-spiffe-id"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowKeeper(tt.spiffeID) assert.False(t, result, "AllowKeeper should return false for: %s", tt.spiffeID) }) } } // TestAllowKeeperPeer_ValidPeers tests that AllowKeeperPeer accepts Nexus and Bootstrap peers func TestAllowKeeperPeer_ValidPeers(t *testing.T) { // Set up environment variables for trust roots os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") os.Setenv("SPIKE_TRUST_ROOT_BOOTSTRAP", "example.org") defer func() { os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") os.Unsetenv("SPIKE_TRUST_ROOT_BOOTSTRAP") }() tests := []struct { name string spiffeID string }{ {"NexusExact", "spiffe://example.org/spike/nexus"}, {"NexusWithInstance", "spiffe://example.org/spike/nexus/instance-1"}, {"BootstrapExact", "spiffe://example.org/spike/bootstrap"}, {"BootstrapWithInstance", "spiffe://example.org/spike/bootstrap/instance-1"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowKeeperPeer(tt.spiffeID) assert.True(t, result, "AllowKeeperPeer should return true for: %s", tt.spiffeID) }) } } // TestAllowKeeperPeer_InvalidPeers tests that AllowKeeperPeer rejects unauthorized peers func TestAllowKeeperPeer_InvalidPeers(t *testing.T) { // Set up environment variables for trust roots os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") os.Setenv("SPIKE_TRUST_ROOT_BOOTSTRAP", "example.org") defer func() { os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") os.Unsetenv("SPIKE_TRUST_ROOT_BOOTSTRAP") }() tests := []struct { name string spiffeID string }{ {"Empty", ""}, {"KeeperSPIFFEID", "spiffe://example.org/spike/keeper"}, {"PilotSPIFFEID", "spiffe://example.org/spike/pilot"}, {"RegularService", "spiffe://example.org/service"}, {"WrongDomain", "spiffe://other.org/spike/nexus"}, {"NotSPIFFEID", "not-a-spiffe-id"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowKeeperPeer(tt.spiffeID) assert.False(t, result, "AllowKeeperPeer should return false for: %s", tt.spiffeID) }) } } // TestPredicate_TypeFunction tests that Predicate type works as expected func TestPredicate_TypeFunction(t *testing.T) { // Create a custom predicate customPredicate := Predicate(func(spiffeID string) bool { return spiffeID == "spiffe://example.org/custom" }) // Test that it works correctly assert.True(t, customPredicate("spiffe://example.org/custom")) assert.False(t, customPredicate("spiffe://example.org/other")) assert.False(t, customPredicate("")) } // TestPredicate_Composition tests that predicates can be composed func TestPredicate_Composition(t *testing.T) { // Create a composite predicate using OR logic allowNexusOrKeeper := func(spiffeID string) bool { return AllowNexus(spiffeID) || AllowKeeper(spiffeID) } // Set up environment variables os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") os.Setenv("SPIKE_TRUST_ROOT_KEEPER", "example.org") defer func() { os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") os.Unsetenv("SPIKE_TRUST_ROOT_KEEPER") }() // Test composition assert.True(t, allowNexusOrKeeper("spiffe://example.org/spike/nexus")) assert.True(t, allowNexusOrKeeper("spiffe://example.org/spike/keeper")) assert.False(t, allowNexusOrKeeper("spiffe://example.org/spike/pilot")) } // TestAllowAll_IsPredicateType tests that AllowAll is of type Predicate func TestAllowAll_IsPredicateType(_ *testing.T) { var _ Predicate = AllowAll } // TestDenyAll_IsPredicateType tests that DenyAll is of type Predicate func TestDenyAll_IsPredicateType(_ *testing.T) { var _ Predicate = DenyAll } // TestAllowNexus_IsPredicateType tests that AllowNexus is of type Predicate func TestAllowNexus_IsPredicateType(_ *testing.T) { var _ Predicate = AllowNexus } // TestAllowKeeper_IsPredicateType tests that AllowKeeper is of type Predicate func TestAllowKeeper_IsPredicateType(_ *testing.T) { var _ Predicate = AllowKeeper } // TestAllowKeeperPeer_MultipleTrustDomains tests AllowKeeperPeer with multiple trust domains func TestAllowKeeperPeer_MultipleTrustDomains(t *testing.T) { // Set up multiple trust domains os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org,dev.example.org") os.Setenv("SPIKE_TRUST_ROOT_BOOTSTRAP", "example.org,dev.example.org") defer func() { os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") os.Unsetenv("SPIKE_TRUST_ROOT_BOOTSTRAP") }() tests := []struct { name string spiffeID string expected bool }{ {"NexusMainDomain", "spiffe://example.org/spike/nexus", true}, {"NexusDevDomain", "spiffe://dev.example.org/spike/nexus", true}, {"BootstrapMainDomain", "spiffe://example.org/spike/bootstrap", true}, {"BootstrapDevDomain", "spiffe://dev.example.org/spike/bootstrap", true}, {"UntrustedDomain", "spiffe://untrusted.org/spike/nexus", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := AllowKeeperPeer(tt.spiffeID) assert.Equal(t, tt.expected, result, "AllowKeeperPeer(%s) should return %v", tt.spiffeID, tt.expected) }) } } spike-sdk-go-0.16.4/qodana.yaml000066400000000000000000000017341511163700700162340ustar00rootroot00000000000000#-------------------------------------------------------------------------------# # Qodana analysis is configured by qodana.yaml file # # https://www.jetbrains.com/help/qodana/qodana-yaml.html # #-------------------------------------------------------------------------------# version: "1.0" #Specify inspection profile for code analysis profile: name: qodana.starter #Enable inspections #include: # - name: #Disable inspections #exclude: # - name: # paths: # - #Execute shell command before Qodana execution (Applied in CI/CD pipeline) #bootstrap: sh ./prepare-qodana.sh #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) #plugins: # - id: #(plugin id can be found at https://plugins.jetbrains.com) #Specify Qodana linter for analysis (Applied in CI/CD pipeline) linter: jetbrains/qodana-go:2024.3 spike-sdk-go-0.16.4/retry/000077500000000000000000000000001511163700700152455ustar00rootroot00000000000000spike-sdk-go-0.16.4/retry/mock/000077500000000000000000000000001511163700700161765ustar00rootroot00000000000000spike-sdk-go-0.16.4/retry/mock/mock.go000066400000000000000000000010531511163700700174550ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package mock import ( "context" ) // Retrier implements Retrier for testing type Retrier struct { RetryFunc func(context.Context, func() error) error } // RetryWithBackoff implements the Retrier interface func (m *Retrier) RetryWithBackoff( ctx context.Context, operation func() error, ) error { if m.RetryFunc != nil { return m.RetryFunc(ctx, operation) } return nil } spike-sdk-go-0.16.4/retry/retry.go000066400000000000000000000363101511163700700167440ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package retry provides a flexible and type-safe retry mechanism with // exponential backoff. It allows for customizable retry strategies and // notifications while maintaining context awareness and cancellation support. package retry import ( "context" "errors" "time" "github.com/cenkalti/backoff/v4" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // Default configuration values for the exponential backoff strategy const ( // Initial wait time between retries defaultInitialInterval = 500 * time.Millisecond // Maximum wait time between retries defaultMaxInterval = 60 * time.Second // Maximum total time for all retry attempts defaultMaxElapsedTime = 1200 * time.Second // A zero max elapsed time means try forever. forever = 0 // Factor by which the wait time increases defaultMultiplier = 2.0 ) // Retrier defines the interface for retry operations with backoff support. // Implementations of this interface provide different retry strategies. type Retrier interface { // RetryWithBackoff executes an operation with a backoff strategy. // It will repeatedly execute the operation until it succeeds or // the context is canceled. The backoff strategy determines the // delay between retry attempts. // // Parameters: // - ctx: Context for cancellation and timeout control // - op: The operation to retry, returns error if the attempt failed // // Returns: // - *sdkErrors.SDKError: nil if successful, or one of the following: // - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached // - ErrRetryContextCanceled: if context is canceled // - The last error from the operation RetryWithBackoff( ctx context.Context, op func() *sdkErrors.SDKError, ) *sdkErrors.SDKError } // TypedRetrier provides type-safe retry operations for functions that return // both a value and an error. It wraps a base Retrier to provide typed results. type TypedRetrier[T any] struct { retrier Retrier } // NewTypedRetrier creates a new TypedRetrier with the given base Retrier. // This allows for type-safe retry operations while reusing existing retry // logic. // // Parameters: // - r: The base Retrier implementation to wrap // // Returns: // - *TypedRetrier[T]: A new TypedRetrier instance for the specified type // // Example: // // retrier := NewTypedRetrier[string](NewExponentialRetrier()) // result, err := retrier.RetryWithBackoff(ctx, func() ( // string, *sdkErrors.SDKError) { // return callExternalService() // }) func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] { return &TypedRetrier[T]{retrier: r} } // RetryWithBackoff executes a typed operation with a backoff strategy. // It preserves the return value while maintaining retry functionality. // // Parameters: // - ctx: Context for cancellation and timeout control // - op: The operation to retry, returns both a value and an error // // Returns: // - T: The result value from the successful operation // - *sdkErrors.SDKError: nil if successful, or one of the following errors: // - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached // - ErrRetryContextCanceled: if context is canceled // - The wrapped error from the operation if it fails func (r *TypedRetrier[T]) RetryWithBackoff( ctx context.Context, op func() (T, *sdkErrors.SDKError), ) (T, *sdkErrors.SDKError) { var result T err := r.retrier.RetryWithBackoff(ctx, func() *sdkErrors.SDKError { var opErr *sdkErrors.SDKError result, opErr = op() return opErr }) return result, err } // NotifyFn is a callback function type for retry notifications. // It provides information about each retry attempt, including the error, // current interval duration, and total elapsed time. type NotifyFn func( err *sdkErrors.SDKError, duration, totalDuration time.Duration, ) // RetrierOption is a function type for configuring ExponentialRetrier. // It follows the functional options pattern for flexible configuration. type RetrierOption func(*ExponentialRetrier) // ExponentialRetrier implements Retrier using exponential backoff strategy. // It provides configurable retry intervals and maximum attempt durations. type ExponentialRetrier struct { newBackOff func() backoff.BackOff notify NotifyFn } // BackOffOption is a function type for configuring ExponentialBackOff. // It allows fine-tuning of the backoff strategy parameters. type BackOffOption func(*backoff.ExponentialBackOff) // NewExponentialRetrier creates a new ExponentialRetrier with configurable // settings. Default values provide sensible backoff behavior for most use // cases. // // Default settings: // - InitialInterval: 500ms // - MaxInterval: 60s // - MaxElapsedTime: 1200s (20 minutes) // - Multiplier: 2.0 // // Parameters: // - opts: Optional configuration functions to customize retry behavior // // Returns: // - *ExponentialRetrier: A configured retrier instance ready for use // // Example: // // retrier := NewExponentialRetrier( // WithBackOffOptions( // WithInitialInterval(100 * time.Millisecond), // WithMaxInterval(5 * time.Second), // ), // WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) { // log.Printf("Retry attempt failed: %v", err) // }), // ) func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier { b := backoff.NewExponentialBackOff() b.InitialInterval = defaultInitialInterval b.MaxInterval = defaultMaxInterval b.MaxElapsedTime = defaultMaxElapsedTime b.Multiplier = defaultMultiplier r := &ExponentialRetrier{ newBackOff: func() backoff.BackOff { return b }, } for _, opt := range opts { opt(r) } return r } // RetryWithBackoff implements the Retrier interface using exponential backoff. // It executes the operation repeatedly until success or context cancellation. // // Parameters: // - ctx: Context for cancellation and timeout control // - operation: The function to retry that returns an error // // Returns: // - *sdkErrors.SDKError: nil if the operation eventually succeeds, or one of: // - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached // - ErrRetryContextCanceled: if context is canceled // - The last error from the operation func (r *ExponentialRetrier) RetryWithBackoff( ctx context.Context, operation func() *sdkErrors.SDKError, ) *sdkErrors.SDKError { b := r.newBackOff() totalDuration := time.Duration(0) // Wrap operation to convert SDKError to plain error for backoff library wrappedOp := func() error { sdkErr := operation() if sdkErr == nil { return nil } return sdkErr } err := backoff.RetryNotify( wrappedOp, backoff.WithContext(b, ctx), func(err error, duration time.Duration) { totalDuration += duration if r.notify != nil { // Convert plain error back to SDKError for notification var sdkErr *sdkErrors.SDKError if errors.As(err, &sdkErr) { r.notify(sdkErr, duration, totalDuration) } else { // Wrap plain error if it's not already an SDKError wrapped := sdkErrors.ErrRetryOperationFailed.Wrap(err) r.notify(wrapped, duration, totalDuration) } } }, ) if err == nil { return nil } // Check if error is already an SDKError var sdkErr *sdkErrors.SDKError if errors.As(err, &sdkErr) { return sdkErr } // Wrap context errors appropriately if errors.Is(err, context.Canceled) { failErr := sdkErrors.ErrRetryContextCanceled.Wrap(err) failErr.Msg = "retry operation canceled" return failErr } if errors.Is(err, context.DeadlineExceeded) { failErr := sdkErrors.ErrRetryMaxElapsedTimeReached.Wrap(err) failErr.Msg = "maximum retry elapsed time exceeded" return failErr } // Wrap any other error failErr := sdkErrors.ErrRetryOperationFailed.Wrap(err) failErr.Msg = "retry operation failed" return failErr } // WithBackOffOptions configures the backoff settings using the provided // options. Multiple options can be combined to customize the retry behavior. // // Parameters: // - opts: One or more BackOffOption functions to configure the backoff // strategy // // Returns: // - RetrierOption: A configuration function for ExponentialRetrier // // Example: // // retrier := NewExponentialRetrier( // WithBackOffOptions( // WithInitialInterval(1 * time.Second), // WithMaxElapsedTime(1 * time.Minute), // ), // ) func WithBackOffOptions(opts ...BackOffOption) RetrierOption { return func(r *ExponentialRetrier) { b := r.newBackOff().(*backoff.ExponentialBackOff) for _, opt := range opts { opt(b) } } } // WithInitialInterval sets the initial interval between retries. // This is the starting point for the exponential backoff calculation. // // Parameters: // - d: The initial wait duration before the first retry // // Returns: // - BackOffOption: A configuration function for ExponentialBackOff func WithInitialInterval(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.InitialInterval = d } } // WithMaxInterval sets the maximum interval between retries. // The interval will never exceed this value, regardless of the multiplier. // // Parameters: // - d: The maximum wait duration between retry attempts // // Returns: // - BackOffOption: A configuration function for ExponentialBackOff func WithMaxInterval(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.MaxInterval = d } } // WithMaxElapsedTime sets the maximum total time for retries. // The retry operation will stop after this duration, even if not successful. // Set to 0 to retry indefinitely (until context is canceled). // // Parameters: // - d: The maximum total duration for all retry attempts // // Returns: // - BackOffOption: A configuration function for ExponentialBackOff func WithMaxElapsedTime(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.MaxElapsedTime = d } } // WithMultiplier sets the multiplier for increasing intervals. // Each retry interval is multiplied by this value, up to MaxInterval. // // Parameters: // - m: The multiplier factor (e.g., 2.0 doubles the interval each time) // // Returns: // - BackOffOption: A configuration function for ExponentialBackOff func WithMultiplier(m float64) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.Multiplier = m } } // WithRandomizationFactor sets the randomization factor for backoff intervals. // The actual interval will be randomized between // [interval * (1 - factor), interval * (1 + factor)]. // // A factor of 0 disables randomization (deterministic intervals). // A factor of 0.5 (the default) means intervals can vary by ±50%. // This randomization helps prevent thundering herd issues in distributed // systems. // // Parameters: // - factor: The randomization factor (0.0 to 1.0) // // Returns: // - BackOffOption: A configuration function for ExponentialBackOff func WithRandomizationFactor(factor float64) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.RandomizationFactor = factor } } // WithNotify is an option to set the notification callback. // The callback is called after each failed attempt, allowing you to log // or monitor retry behavior. // // Parameters: // - fn: Callback function invoked after each failed retry attempt // // Returns: // - RetrierOption: A configuration function for ExponentialRetrier // // Example: // // retrier := NewExponentialRetrier( // WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) { // log.Printf("Attempt failed after %v, total time %v: %v", // d, total, err) // }), // ) func WithNotify(fn NotifyFn) RetrierOption { return func(r *ExponentialRetrier) { r.notify = fn } } // Handler represents a function that returns a value and an error. // It's used with the Do helper function for simple retry operations. type Handler[T any] func() (T, *sdkErrors.SDKError) // Do provides a simplified way to retry a typed operation with configurable // settings. It creates a TypedRetrier with exponential backoff and applies // any provided options. // // This is a convenience function for common retry scenarios where you don't // need to create and manage a retrier instance explicitly. // // Parameters: // - ctx: Context for cancellation and timeout control // - handler: The function to retry that returns a value and error // - options: Optional configuration for the retry behavior // // Returns: // - T: The result value from the successful operation // - *sdkErrors.SDKError: nil if successful, or one of the following errors: // - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached // - ErrRetryContextCanceled: if context is canceled // - The wrapped error from the handler if it fails // // Example: // // result, err := Do(ctx, func() (string, *sdkErrors.SDKError) { // return fetchData() // }, WithNotify(logRetryAttempts)) func Do[T any]( ctx context.Context, handler Handler[T], options ...RetrierOption, ) (T, *sdkErrors.SDKError) { return NewTypedRetrier[T]( NewExponentialRetrier(options...), ).RetryWithBackoff(ctx, handler) } // Forever retries an operation indefinitely with exponential backoff until it // succeeds or the context is canceled. It sets MaxElapsedTime to 0, which means // the retry loop will continue forever (or until the context is canceled). // // This is a convenience function that sets up exponential backoff with sensible // defaults for infinite retry scenarios. // // Default settings: // - InitialInterval: 500ms // - MaxInterval: 60s // - MaxElapsedTime: 0 (retry forever) // - Multiplier: 2.0 // // Parameters: // - ctx: Context for cancellation control (the only way to stop retrying) // - handler: The function to retry that returns a value and error // - options: Optional configuration for retry behavior // // Note: User-provided options are applied AFTER the default settings and will // override them. If you pass WithBackOffOptions(WithMaxElapsedTime(...)), it // will override the "forever" behavior. This allows power users to customize // the retry behavior while keeping the convenience of preset defaults. // // Returns: // - T: The result value from the successful operation // - *sdkErrors.SDKError: nil if successful, or one of the following errors: // - ErrRetryContextCanceled: if context is canceled // - The wrapped error from the handler if all retries fail // // Example: // // // Retry forever with custom notification // result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) { // return fetchData() // }, WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) { // log.Printf("Retry failed: %v (attempt duration: %v, total: %v)", // err, d, total) // })) // // // Override behavior (will now stop after 1 minute // // instead of retrying forever) // result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) { // return fetchData() // }, WithBackOffOptions(WithMaxElapsedTime(1 * time.Minute))) func Forever[T any]( ctx context.Context, handler Handler[T], options ...RetrierOption, ) (T, *sdkErrors.SDKError) { ro := WithBackOffOptions(WithMaxElapsedTime(forever)) ros := []RetrierOption{ro} ros = append(ros, options...) return NewTypedRetrier[T]( NewExponentialRetrier(ros...), ).RetryWithBackoff(ctx, handler) } spike-sdk-go-0.16.4/retry/retry_test.go000066400000000000000000000261121511163700700200020ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package retry import ( "context" "testing" "time" "github.com/cenkalti/backoff/v4" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/stretchr/testify/assert" ) func TestExponentialRetrier_Success(t *testing.T) { retrier := NewExponentialRetrier() // Operation that succeeds immediately err := retrier.RetryWithBackoff(context.Background(), func() *sdkErrors.SDKError { return nil }) assert.Nil(t, err) } func TestExponentialRetrier_EventualSuccess(t *testing.T) { attempts := 0 maxAttempts := 3 retrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(1*time.Millisecond), WithMaxInterval(5*time.Millisecond), ), ) err := retrier.RetryWithBackoff(context.Background(), func() *sdkErrors.SDKError { attempts++ if attempts < maxAttempts { return sdkErrors.ErrRetryOperationFailed } return nil }) assert.Nil(t, err) assert.Equal(t, maxAttempts, attempts) } func TestExponentialRetrier_Failure(t *testing.T) { retrier := NewExponentialRetrier( WithBackOffOptions( WithMaxElapsedTime(10*time.Millisecond), WithInitialInterval(1*time.Millisecond), ), ) expectedErr := sdkErrors.ErrRetryOperationFailed err := retrier.RetryWithBackoff(context.Background(), func() *sdkErrors.SDKError { return expectedErr }) assert.Error(t, err) assert.True(t, err.Is(expectedErr)) } func TestExponentialRetrier_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) retrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(100 * time.Millisecond), ), ) go func() { time.Sleep(50 * time.Millisecond) cancel() }() err := retrier.RetryWithBackoff(ctx, func() *sdkErrors.SDKError { return sdkErrors.ErrRetryOperationFailed }) assert.Error(t, err) assert.True(t, err.Is(sdkErrors.ErrRetryContextCanceled)) } func TestExponentialRetrier_ContextDeadlineExceeded(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() retrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(100 * time.Millisecond), ), ) err := retrier.RetryWithBackoff(ctx, func() *sdkErrors.SDKError { return sdkErrors.ErrRetryOperationFailed }) assert.Error(t, err) // Context deadline exceeded should be wrapped as ErrRetryMaxElapsedTimeReached assert.True(t, err.Is(sdkErrors.ErrRetryMaxElapsedTimeReached)) } func TestExponentialRetrier_Notification(t *testing.T) { var notifications []time.Duration totalDurations := make([]time.Duration, 0) retrier := NewExponentialRetrier( WithNotify(func(_ *sdkErrors.SDKError, duration, totalDuration time.Duration) { notifications = append(notifications, duration) totalDurations = append(totalDurations, totalDuration) }), WithBackOffOptions( // NOTE: These intervals are intentionally set to reasonable values // (10ms+) to ensure test reliability across different systems and // CI environments. DO NOT reduce these values without good reason: // // 1. System timer precision: Many systems have timer resolution // of 1-15ms, making sub-millisecond intervals unreliable // 2. Scheduling jitter: OS thread scheduling can cause actual // sleep durations to vary significantly from intended values // 3. CI environment variance: Different CI systems (GitHub Actions, // local Docker, etc.) have different performance characteristics // 4. Test determinism: Smaller intervals make the test more likely // to fail due to timing race conditions // // If you need faster tests, consider mocking time instead of // reducing these intervals. WithInitialInterval(100*time.Millisecond), WithMaxInterval(500*time.Millisecond), WithMaxElapsedTime(2000*time.Millisecond), // Disable randomization (jitter) to ensure deterministic intervals // for testing. By default, backoff uses a RandomizationFactor of 0.5, // which randomizes intervals by ±50%. This can cause the second retry // to have a shorter duration than the first (e.g., first retry: 150ms, // second retry: 100ms), breaking the monotonically increasing assumption // in our assertions below (lines 134-135). WithRandomizationFactor(0), ), ) attempts := 0 _ = retrier.RetryWithBackoff(context.Background(), func() *sdkErrors.SDKError { attempts++ if attempts < 3 { return sdkErrors.ErrRetryOperationFailed } return nil }) assert.Equal(t, 2, len(notifications)) assert.Equal(t, 2, len(totalDurations)) // Verify that durations are increasing assert.Less(t, notifications[0], notifications[1]) assert.Less(t, totalDurations[0], totalDurations[1]) } func TestTypedRetrier_Success(t *testing.T) { baseRetrier := NewExponentialRetrier() typedRetrier := NewTypedRetrier[string](baseRetrier) expected := "success" result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (string, *sdkErrors.SDKError) { return expected, nil }) assert.Nil(t, err) assert.Equal(t, expected, result) } func TestTypedRetrier_EventualSuccess(t *testing.T) { baseRetrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(1*time.Millisecond), WithMaxInterval(5*time.Millisecond), ), ) typedRetrier := NewTypedRetrier[string](baseRetrier) attempts := 0 expected := "final-value" result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (string, *sdkErrors.SDKError) { attempts++ if attempts < 3 { return "", sdkErrors.ErrRetryOperationFailed } return expected, nil }) assert.Nil(t, err) assert.Equal(t, expected, result) assert.Equal(t, 3, attempts) } func TestTypedRetrier_Failure(t *testing.T) { baseRetrier := NewExponentialRetrier( WithBackOffOptions( WithMaxElapsedTime(10*time.Millisecond), WithInitialInterval(1*time.Millisecond), ), ) typedRetrier := NewTypedRetrier[int](baseRetrier) expectedErr := sdkErrors.ErrRetryOperationFailed result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (int, *sdkErrors.SDKError) { return 0, expectedErr }) assert.Error(t, err) assert.True(t, err.Is(expectedErr)) assert.Equal(t, 0, result) } func TestTypedRetrier_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) baseRetrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(100 * time.Millisecond), ), ) typedRetrier := NewTypedRetrier[string](baseRetrier) go func() { time.Sleep(50 * time.Millisecond) cancel() }() result, err := typedRetrier.RetryWithBackoff(ctx, func() (string, *sdkErrors.SDKError) { return "", sdkErrors.ErrRetryOperationFailed }) assert.Error(t, err) assert.True(t, err.Is(sdkErrors.ErrRetryContextCanceled)) assert.Equal(t, "", result) } func TestDo_Success(t *testing.T) { expected := "success-value" result, err := Do(context.Background(), func() (string, *sdkErrors.SDKError) { return expected, nil }) assert.Nil(t, err) assert.Equal(t, expected, result) } func TestDo_Failure(t *testing.T) { expectedErr := sdkErrors.ErrRetryOperationFailed result, err := Do( context.Background(), func() (int, *sdkErrors.SDKError) { return 0, expectedErr }, WithBackOffOptions( WithMaxElapsedTime(10*time.Millisecond), WithInitialInterval(1*time.Millisecond), ), ) assert.Error(t, err) assert.True(t, err.Is(expectedErr)) assert.Equal(t, 0, result) } func TestDo_WithOptions(t *testing.T) { attempts := 0 expected := "eventual-success" result, err := Do( context.Background(), func() (string, *sdkErrors.SDKError) { attempts++ if attempts < 3 { return "", sdkErrors.ErrRetryOperationFailed } return expected, nil }, WithBackOffOptions( WithInitialInterval(1*time.Millisecond), WithMaxInterval(5*time.Millisecond), ), ) assert.Nil(t, err) assert.Equal(t, expected, result) assert.Equal(t, 3, attempts) } func TestForever_EventualSuccess(t *testing.T) { attempts := 0 expected := "eventual-success" result, err := Forever( context.Background(), func() (string, *sdkErrors.SDKError) { attempts++ if attempts < 5 { return "", sdkErrors.ErrRetryOperationFailed } return expected, nil }, WithBackOffOptions( WithInitialInterval(1*time.Millisecond), WithMaxInterval(5*time.Millisecond), ), ) assert.Nil(t, err) assert.Equal(t, expected, result) assert.Equal(t, 5, attempts) } func TestForever_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) attempts := 0 go func() { time.Sleep(50 * time.Millisecond) cancel() }() result, err := Forever( ctx, func() (string, *sdkErrors.SDKError) { attempts++ return "", sdkErrors.ErrRetryOperationFailed }, WithBackOffOptions( WithInitialInterval(1*time.Millisecond), ), ) assert.Error(t, err) assert.True(t, err.Is(sdkErrors.ErrRetryContextCanceled)) assert.Equal(t, "", result) // Should have retried multiple times before cancellation assert.Greater(t, attempts, 1) } func TestForever_UserOverride(t *testing.T) { attempts := 0 // User overrides Forever's MaxElapsedTime=0 with a specific value // This should stop retrying after 10ms result, err := Forever( context.Background(), func() (int, *sdkErrors.SDKError) { attempts++ return 0, sdkErrors.ErrRetryOperationFailed }, WithBackOffOptions( WithMaxElapsedTime(10*time.Millisecond), WithInitialInterval(1*time.Millisecond), ), ) assert.Error(t, err) assert.True(t, err.Is(sdkErrors.ErrRetryOperationFailed)) assert.Equal(t, 0, result) // Should have stopped after MaxElapsedTime, not retry forever assert.Greater(t, attempts, 1) assert.Less(t, attempts, 100) // Shouldn't retry too many times } func TestForever_VerifiesMaxElapsedTimeZero(t *testing.T) { // This test verifies that Forever actually sets MaxElapsedTime to 0 // by checking the backoff configuration retrier := NewExponentialRetrier( WithBackOffOptions(WithMaxElapsedTime(forever)), ) b := retrier.newBackOff().(*backoff.ExponentialBackOff) assert.Equal(t, time.Duration(0), b.MaxElapsedTime) } func TestBackOffOptions(t *testing.T) { initialInterval := 100 * time.Millisecond maxInterval := 1 * time.Second maxElapsedTime := 5 * time.Second multiplier := 2.5 retrier := NewExponentialRetrier( WithBackOffOptions( WithInitialInterval(initialInterval), WithMaxInterval(maxInterval), WithMaxElapsedTime(maxElapsedTime), WithMultiplier(multiplier), ), ) // Access the backoff configuration b := retrier.newBackOff().(*backoff.ExponentialBackOff) assert.Equal(t, initialInterval, b.InitialInterval) assert.Equal(t, maxInterval, b.MaxInterval) assert.Equal(t, maxElapsedTime, b.MaxElapsedTime) assert.Equal(t, multiplier, b.Multiplier) } func TestDefaultSettings(t *testing.T) { retrier := NewExponentialRetrier() b := retrier.newBackOff().(*backoff.ExponentialBackOff) assert.Equal(t, defaultInitialInterval, b.InitialInterval) assert.Equal(t, defaultMaxInterval, b.MaxInterval) assert.Equal(t, defaultMaxElapsedTime, b.MaxElapsedTime) assert.Equal(t, defaultMultiplier, b.Multiplier) } spike-sdk-go-0.16.4/security/000077500000000000000000000000001511163700700157475ustar00rootroot00000000000000spike-sdk-go-0.16.4/security/mem/000077500000000000000000000000001511163700700165255ustar00rootroot00000000000000spike-sdk-go-0.16.4/security/mem/doc.go000066400000000000000000000032121511163700700176170ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package mem provides utilities for secure memory operations to protect // sensitive data such as cryptographic keys and secrets. // // The package includes functionality for: // - Securely erasing memory by overwriting with zeros or multiple patterns // - Preventing memory from being swapped to disk (Unix-like systems) // - Checking if memory regions contain only zeros // - Handling both fixed-size arrays and byte slices // // Memory Clearing: // // ClearRawBytes performs a single-pass zero overwrite, which is sufficient // for most use cases according to NIST SP 800-88 Rev. 1 guidelines: // // key := &[32]byte{...} // defer mem.ClearRawBytes(key) // // ClearRawBytesParanoid performs multiple passes with different patterns // for extreme security requirements: // // sensitiveData := &[64]byte{...} // defer mem.ClearRawBytesParanoid(sensitiveData) // // For byte slices, use ClearBytes to clear the underlying array data: // // token := []byte("secret-token") // defer mem.ClearBytes(token) // // Memory Locking: // // Lock prevents process memory from being swapped to disk, reducing the risk // of secrets being written to persistent storage: // // if mem.Lock() { // // Memory is locked, proceed with sensitive operations // } // // Important Notes: // // ClearRawBytes only clears the direct memory of the provided value. For // structs containing pointers, slices, or maps, you must clear the referenced // data separately before clearing the struct itself. package mem spike-sdk-go-0.16.4/security/mem/lock.go000066400000000000000000000012571511163700700200110ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 //go:build !windows package mem import ( "syscall" "github.com/spiffe/spike-sdk-go/log" ) // Lock attempts to lock the process memory to prevent swapping. // Returns true if successful, false if not supported or failed. func Lock() bool { const fName = "Lock" // Attempt to lock all current and future memory if err := syscall.Mlockall( syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil { log.Log().Warn(fName, "msg", "Failed to lock memory", "err", err.Error()) return false } return true } spike-sdk-go-0.16.4/security/mem/lock_windows.go000066400000000000000000000007431511163700700215620ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 //go:build windows // Package mem provides utilities for secure mem operations. package mem // Lock attempts to lock the process memory to prevent swapping. // Returns true if successful, false if not supported or failed. func Lock() bool { // `mlock` is only available on Unix-like systems return false } spike-sdk-go-0.16.4/security/mem/secure.go000066400000000000000000000151411511163700700203440ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package mem import ( "crypto/rand" "runtime" "unsafe" "github.com/spiffe/spike-sdk-go/log" ) // ClearRawBytes securely erases all bytes in the provided value by overwriting // its memory with zeros. This ensures sensitive data like cryptographic keys // and Shamir shards are properly cleaned from memory before garbage collection. // // According to NIST SP 800-88 Rev. 1 (Guidelines for Media Sanitization), // a single overwrite pass with zeros is sufficient for modern storage // devices, including RAM. // // IMPORTANT LIMITATIONS: // // This function only clears the direct memory occupied by the struct/value. // It does NOT clear data referenced by pointers, slices, maps, or channels. // For structs containing reference types, you must clear the referenced // data separately before calling this function. // // Examples of what is NOT cleared: // - Data pointed to by pointers within the struct // - Underlying arrays of slices // - Keys and values in maps // - Immutable string data (only the string header is cleared) // - Data in channels // // APPROPRIATE USE CASES: // - Fixed-size byte arrays: [32]byte, [64]byte, etc. // - Structs containing only value types (no pointers/slices/maps) // - Primitive types: int, float64, bool, etc. // - Arrays of primitive types // // INAPPROPRIATE USE CASES: // - Structs with pointer fields (unless you only want to clear the pointers) // - Slices, maps, channels, interfaces // - Structs with embedded reference types // // For general struct clearing with proper Go semantics, consider: // // var zero T // *s = zero // // Parameters: // - s: A pointer to any type of data that should be securely erased // // Usage examples: // // // GOOD: Fixed-size byte array // key := &[32]byte{...} // defer ClearRawBytes(key) // // // GOOD: Struct with only value types // type Coordinates struct { // X, Y, Z float64 // Valid bool // } // coords := &Coordinates{...} // defer ClearRawBytes(coords) // // // CAUTION: Struct with pointers - only clears the pointer values // type MixedData struct { // Key [32]byte // This will be cleared // Secret *string // Only the pointer is cleared, not the string data // Tokens []byte // Only slice header is cleared, not the underlying // array // } // data := &MixedData{...} // // Clear referenced data first: // ClearRawBytes(data.Secret) // Clear the string (if it points to a fixed // // array) // ClearRawBytes(&data.Tokens[0]) // Clear slice data manually if needed // ClearRawBytes(data) // Finally clear the struct itself. func ClearRawBytes[T any](s *T) { if s == nil { return } p := unsafe.Pointer(s) size := unsafe.Sizeof(*s) b := (*[1 << 30]byte)(p)[:size:size] // Zero out all bytes in mem for i := range b { b[i] = 0 } // Make sure the data is actually wiped before gc has time to interfere runtime.KeepAlive(s) } // ClearRawBytesParanoid provides a more thorough memory wiping method for // highly sensitive data. // // It performs multiple passes using different patterns (zeros, ones, // random data, and alternating bits) to minimize potential data remanence // concerns from sophisticated physical memory attacks. // // This method is designed for extremely security-sensitive applications where: // 1. An attacker might have physical access to RAM // 2. Cold boot attacks or specialized memory forensics equipment might be // used // 3. The data being protected is critically sensitive (e.g., high-value // encryption keys) // // For most applications, the standard ClearRawBytes() method is sufficient as: // - Modern RAM technologies (DDR4/DDR5) make data remanence attacks // increasingly difficult // - Successful attacks typically require specialized equipment and immediate // (sub-second) physical access. // - The time window for such attacks is extremely short after power loss // - The detectable signal from previous memory states diminishes rapidly with // a single overwrite // // This method is provided for users with extreme security requirements or in // regulated environments where multiple-pass overwrite policies are mandated. func ClearRawBytesParanoid[T any](s *T) { const fName = "ClearRawBytesParanoid" if s == nil { return } p := unsafe.Pointer(s) size := unsafe.Sizeof(*s) b := (*[1 << 30]byte)(p)[:size:size] // Pattern overwrite cycles: // 1. All zeros // 2. All ones (0xFF) // 3. Random data // 4. Alternating 0x55/0xAA (01010101/10101010) // 5. Final zero out // Zero out all bytes (first pass) for i := range b { b[i] = 0 } runtime.KeepAlive(s) // Fill with ones (second pass) for i := range b { b[i] = 0xFF } runtime.KeepAlive(s) // Fill with random data (third pass) _, err := rand.Read(b) if err != nil { log.FatalLn(fName) } runtime.KeepAlive(s) // Alternating bit pattern (fourth pass) for i := range b { if i%2 == 0 { b[i] = 0x55 // 01010101 } else { b[i] = 0xAA // 10101010 } } runtime.KeepAlive(s) // Final zero out (fifth pass) for i := range b { b[i] = 0 } runtime.KeepAlive(s) } // Zeroed32 checks if a 32-byte array contains only zero values. // Returns true if all bytes are zero, false otherwise. func Zeroed32(ar *[32]byte) bool { for _, v := range ar { if v != 0 { return false } } return true } // ClearBytes securely erases a byte slice by overwriting all bytes with zeros. // This is a convenience wrapper around Clear for byte slices. // // This is especially important for slices because executing `mem.Clear` on // a slice, it will only zero out the slice header structure itself, NOT the // underlying array data that the slice points to. // // When we pass a byte slice s to the function Clear[T any](s *T), // we are passing a pointer to the slice header, not a pointer to the // underlying array. The slice header contains three fields: // - A pointer to the underlying array // - The length of the slice // - The capacity of the slice // // mem.Clear(s) will zero out this slice header structure, but not the // actual array data the slice points to // // Parameters: // - b: A byte slice that should be securely erased // // Usage: // // key := []byte{...} // Sensitive cryptographic key // defer mem.ClearBytes(key) // // Use key... func ClearBytes(b []byte) { if len(b) == 0 { return } for i := range b { b[i] = 0 } // Make sure the data is actually wiped before gc has time to interfere runtime.KeepAlive(b) } spike-sdk-go-0.16.4/security/mem/secure_test.go000066400000000000000000000031241511163700700214010ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package mem import ( "testing" ) func TestClear(t *testing.T) { type testStruct struct { Key [32]byte Token string UserID int64 } // Create test data with non-zero values key := [32]byte{} for i := range key { key[i] = byte(i + 1) } data := &testStruct{ Key: key, Token: "secret-token-value", UserID: 12345, } // Call Clear on the data ClearRawBytes(data) // Verify all fields are zeroed for i, b := range data.Key { if b != 0 { t.Errorf("Expected byte at index %d to be 0, got %d", i, b) } } // Note: String contents won't be zeroed directly as strings are immutable in Go // The string header will point to the same backing array // In a real application, sensitive strings should be stored as byte slices if data.UserID != 0 { t.Errorf("Expected UserID to be 0, got %d", data.UserID) } } func TestClearBytes(t *testing.T) { // Create a non-zero byte slice bytes := make([]byte, 64) for i := range bytes { bytes[i] = byte(i + 1) } // Make a copy to verify later original := make([]byte, len(bytes)) copy(original, bytes) // Verify bytes are non-zero initially for i, b := range bytes { if b != original[i] { t.Fatalf("Test setup issue: bytes changed before ClearBytes call") } } // Call ClearBytes ClearBytes(bytes) // Verify all bytes are zeroed for i, b := range bytes { if b != 0 { t.Errorf("Expected byte at index %d to be 0, got %d", i, b) } } } spike-sdk-go-0.16.4/spiffe/000077500000000000000000000000001511163700700153545ustar00rootroot00000000000000spike-sdk-go-0.16.4/spiffe/doc.go000066400000000000000000000025311511163700700164510ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package spiffe provides utilities for working with SPIFFE (Secure Production // Identity Framework for Everyone) and the SPIFFE Workload API. // // The package includes functionality for: // - Discovering and connecting to SPIFFE Workload API endpoints // - Creating and managing X.509 SVID sources for workload identity // - Extracting SPIFFE IDs from HTTP requests over mTLS connections // - Proper resource cleanup and connection management // // SPIFFE provides a standard way to identify and authenticate workloads in // distributed systems. This package simplifies the integration with SPIFFE // infrastructure by handling the connection setup, certificate management, // and identity extraction. // // Example usage: // // // Get the SPIFFE Workload API socket // socket := spiffe.EndpointSocket() // // // Create an X.509 source // source, svidID, err := spiffe.Source(context.Background(), socket) // if err != nil { // log.Fatal(err) // } // defer spiffe.CloseSource(source) // // // Extract SPIFFE ID from an mTLS HTTP request // spiffeID, err := spiffe.IDFromRequest(req) // if err != nil { // log.Printf("Failed to extract SPIFFE ID: %v", err) // } package spiffe spike-sdk-go-0.16.4/spiffe/spiffe.go000066400000000000000000000111021511163700700171520ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package spiffe import ( "context" "net/http" "os" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/config/env" ) // EndpointSocket returns the UNIX domain socket address for the SPIFFE // Workload API endpoint. // // The function first checks for the SPIFFE_ENDPOINT_SOCKET environment // variable. If set, it returns that value. Otherwise, it returns a default // development // // socket path: // // "unix:///tmp/spire-agent/public/api.sock" // // For production deployments, especially in Kubernetes environments, it's // recommended to set SPIFFE_ENDPOINT_SOCKET to a more restricted socket path, // such as: "unix:///run/spire/agent/sockets/spire.sock" // // Default socket paths by environment: // - Development (Linux): unix:///tmp/spire-agent/public/api.sock // - Kubernetes: unix:///run/spire/agent/sockets/spire.sock // // Returns: // - string: The UNIX domain socket address for the SPIFFE Workload API // endpoint // // Environment Variables: // - SPIFFE_ENDPOINT_SOCKET: Override the default socket path func EndpointSocket() string { p := os.Getenv(env.SPIFFEEndpointSocket) if p != "" { return p } return "unix:///tmp/spire-agent/public/api.sock" } // Source creates a new SPIFFE X.509 source and returns the associated SVID ID. // It establishes a connection to the Workload API at the specified socket path // and retrieves the X.509 SVID for the workload. // // The returned X509Source should be closed when no longer needed. // // Parameters: // - ctx: Context for cancellation and timeout control // - socketPath: The Workload API endpoint location // (e.g., "unix:///path/to/socket") // // Returns: // - *workloadapi.X509Source: An X509Source that can be used to fetch and // monitor X.509 SVIDs // - string: The string representation of the current SVID ID // - *sdkErrors.SDKError: ErrSPIFFEFailedToCreateX509Source if source creation // fails, or ErrSPIFFEUnableToFetchX509Source if initial SVID fetch fails func Source(ctx context.Context, socketPath string) ( *workloadapi.X509Source, string, *sdkErrors.SDKError, ) { source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath))) if err != nil { return nil, "", sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err) } sv, err := source.GetX509SVID() if err != nil { return nil, "", sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err) } return source, sv.ID.String(), nil } // IDFromRequest extracts the SPIFFE ID from the TLS peer certificate of // an HTTP request. It checks if the incoming request has a valid TLS connection // and at least one peer certificate. The first certificate in the chain is used // to extract the SPIFFE ID. // // Note: This function assumes that the request is already over a secured TLS // connection and will fail if the TLS connection state is not available or // the peer certificates are missing. // // Parameters: // - r: The HTTP request from which the SPIFFE ID is to be extracted // // Returns: // - *spiffeid.ID: The SPIFFE ID extracted from the first peer certificate, // or nil if extraction fails // - *sdkErrors.SDKError: ErrSPIFFENoPeerCertificates if peer certificates are // absent, or ErrSPIFFEFailedToExtractX509SVID if extraction fails func IDFromRequest(r *http.Request) (*spiffeid.ID, *sdkErrors.SDKError) { tlsConnectionState := r.TLS if len(tlsConnectionState.PeerCertificates) == 0 { return nil, sdkErrors.ErrSPIFFENoPeerCertificates } id, err := x509svid.IDFromCert(tlsConnectionState.PeerCertificates[0]) if err != nil { return nil, sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err) } return &id, nil } // CloseSource safely closes an X509Source. // // This function should be called when the X509Source is no longer needed, // typically during application shutdown or cleanup. It handles nil sources // gracefully. // // Parameters: // - source: The X509Source to close, may be nil // // Returns: // - *sdkErrors.SDKError: nil if successful or source is nil, // ErrSPIFFEFailedToCloseX509Source if closure fails func CloseSource(source *workloadapi.X509Source) *sdkErrors.SDKError { if source == nil { return nil } if err := source.Close(); err != nil { return sdkErrors.ErrSPIFFEFailedToCloseX509Source.Wrap(err) } return nil } spike-sdk-go-0.16.4/spiffeid/000077500000000000000000000000001511163700700156715ustar00rootroot00000000000000spike-sdk-go-0.16.4/spiffeid/auth.go000066400000000000000000000305511511163700700171650ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package spiffeid import ( "strings" "github.com/spiffe/spike-sdk-go/config/env" ) // IsPilot checks if a given SPIFFE ID matches the SPIKE Pilot's SPIFFE ID // pattern. // // This function is used for identity verification to determine if the provided // SPIFFE ID belongs to a SPIKE pilot instance. It compares the input against // the expected pilot SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/pilot" // - Extended match with metadata: // "spiffe:///spike/pilot/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base pilot identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact pilot ID // or an extended ID with additional path segments for any of the trust // roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/pilot" // extendedId := "spiffe://example.org/spike/pilot/instance-0" // // // Both will return true // if IsPilot(baseId) { // // Handle pilot-specific logic // } // // if IsPilot(extendedId) { // // Also recognized as a SPIKE Pilot, with instance metadata // } func IsPilot(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootPilot) for _, root := range strings.Split(trustRoots, ",") { baseID := Pilot(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsLiteWorkload checks if a given SPIFFE ID matches the SPIKE Lite Workload's // SPIFFE ID pattern. // // A SPIKE Lite workload can freely use SPIKE Nexus encryption and decryption // RESTful APIs without needing any specific policies assigned to it. A SPIKE // Lite workload cannot use any other SPIKE Nexus API unless a relevant policy // is attached to it. // // This function is used for identity verification to determine if the provided // SPIFFE ID belongs to a SPIKE lite workload instance. It compares the input // against the expected lite workload SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/workload/role/lite" // - Extended match with metadata: // "spiffe:///spike/workload/role/lite/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base lite workload identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact lite // workload ID or an extended ID with additional path segments for any of // the trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/workload/role/lite" // extendedId := "spiffe://example.org/spike/workload/role/lite/instance-0" // // // Both will return true // if IsLiteWorkload(baseId) { // // Handle lite workload-specific logic // } // // if IsLiteWorkload(extendedId) { // // Also recognized as a SPIKE Lite Workload, with instance metadata // } func IsLiteWorkload(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootLiteWorkload) for _, root := range strings.Split(trustRoots, ",") { baseID := LiteWorkload(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsPilotRecover checks if a given SPIFFE ID matches the SPIKE Pilot's // recovery SPIFFE ID pattern. // // This function verifies if the provided SPIFFE ID corresponds to a SPIKE Pilot // instance with recovery capabilities by comparing it against the expected // recovery SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/pilot/recover" // - Extended match with metadata: // "spiffe:///spike/pilot/recover/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base pilot recovery identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact pilot // recovery ID or an extended ID with additional path segments for any of // the trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/pilot/recover" // extendedId := "spiffe://example.org/spike/pilot/recover/instance-0" // // // Both will return true // if IsPilotRecover(baseId) { // // Handle recovery-specific logic // } // // if IsPilotRecover(extendedId) { // // Also recognized as a SPIKE Pilot recovery, with instance metadata // } func IsPilotRecover(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootPilot) for _, root := range strings.Split(trustRoots, ",") { baseID := PilotRecover(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsPilotRestore checks if a given SPIFFE ID matches the SPIKE Pilot's restore // SPIFFE ID pattern. // // This function verifies if the provided SPIFFE ID corresponds to a pilot // instance with restore capabilities by comparing it against the expected // restore SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/pilot/restore" // - Extended match with metadata: // "spiffe:///spike/pilot/restore/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base pilot restore identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact pilot // restore ID or an extended ID with additional path segments for any of the // trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/pilot/restore" // extendedId := "spiffe://example.org/spike/pilot/restore/instance-0" // // // Both will return true // if IsPilotRestore(baseId) { // // Handle restore-specific logic // } // // if IsPilotRestore(extendedId) { // // Also recognized as a SPIKE Pilot restore, with instance metadata // } func IsPilotRestore(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootPilot) for _, root := range strings.Split(trustRoots, ",") { baseID := PilotRestore(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsBootstrap checks if a given SPIFFE ID matches the SPIKE Bootstrap's // SPIFFE ID pattern. // // This function verifies if the provided SPIFFE ID corresponds to a bootstrap // instance by comparing it against the expected bootstrap SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/bootstrap" // - Extended match with metadata: // "spiffe:///spike/bootstrap/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base bootstrap identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact bootstrap // ID or an extended ID with additional path segments for any of the // trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/bootstrap" // extendedId := "spiffe://example.org/spike/bootstrap/instance-0" // // // Both will return true // if IsBootstrap(baseId) { // // Handle bootstrap-specific logic // } // // if IsBootstrap(extendedId) { // // Also recognized as a SPIKE Bootstrap, with instance metadata // } func IsBootstrap(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootBootstrap) for _, root := range strings.Split(trustRoots, ",") { baseID := Bootstrap(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsKeeper checks if a given SPIFFE ID matches the SPIKE Keeper's SPIFFE ID. // // This function is used for identity verification to determine if the provided // SPIFFE ID belongs to a SPIKE Keeper instance. It compares the input against // the expected keeper SPIFFE ID pattern. // // The function supports two formats: // - Exact match: "spiffe:///spike/keeper" // - Extended match with metadata: // "spiffe:///spike/keeper/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base keeper identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the provided SPIFFE ID matches either the exact // SPIKE Keeper's ID or an extended ID with additional path segments for any // of the trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/keeper" // extendedId := "spiffe://example.org/spike/keeper/instance-0" // // // Both will return true // if IsKeeper(baseId) { // // Handle keeper-specific logic // } // // if IsKeeper(extendedId) { // // Also recognized as a SPIKE Keeper, with instance metadata // } func IsKeeper(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootKeeper) for _, root := range strings.Split(trustRoots, ",") { baseID := Keeper(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // IsNexus checks if the provided SPIFFE ID matches the SPIKE Nexus SPIFFE ID. // // The function compares the input SPIFFE ID against the configured SPIKE Nexus // SPIFFE ID pattern. This is typically used for validating whether a given // identity represents the Nexus service. // // The function supports two formats: // - Exact match: "spiffe:///spike/nexus" // - Extended match with metadata: // "spiffe:///spike/nexus/" // // This allows for instance-specific identifiers while maintaining compatibility // with the base Nexus identity. // // Parameters: // - SPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the SPIFFE ID matches either the exact Nexus SPIFFE ID // or an extended ID with additional path segments for any of the // trust roots, false otherwise // // Example usage: // // baseId := "spiffe://example.org/spike/nexus" // extendedId := "spiffe://example.org/spike/nexus/instance-0" // // // Both will return true // if IsNexus(baseId) { // // Handle Nexus-specific logic // } // // if IsNexus(extendedId) { // // Also recognized as a SPIKE Nexus, with instance metadata // } func IsNexus(SPIFFEID string) bool { trustRoots := env.TrustRootFromEnv(env.TrustRootNexus) for _, root := range strings.Split(trustRoots, ",") { baseID := Nexus(strings.TrimSpace(root)) // Check if the ID is either exactly the base ID or starts with the base ID // followed by "/" if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") { return true } } return false } // PeerCanTalkToAnyone is used for debugging purposes func PeerCanTalkToAnyone(_, _ string) bool { return true } // PeerCanTalkToKeeper checks if the provided SPIFFE ID matches the SPIKE Nexus // SPIFFE ID. // // This is used as a validator in SPIKE Keeper because currently only SPIKE // Nexus can talk to SPIKE Keeper. // // Parameters: // - peerSPIFFEID: The SPIFFE ID string to check // // Returns: // - bool: true if the SPIFFE ID matches SPIKE Nexus' or SPIKE Bootstrap's // SPIFFE ID for any of the trust roots, false otherwise func PeerCanTalkToKeeper(peerSPIFFEID string) bool { return IsNexus(peerSPIFFEID) || IsBootstrap(peerSPIFFEID) } spike-sdk-go-0.16.4/spiffeid/doc.go000066400000000000000000000040001511163700700167570ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package spiffeid provides utilities for constructing and validating // SPIFFE IDs for SPIKE system components. // // The package includes functionality for: // - Constructing standardized SPIFFE IDs for SPIKE components (Nexus, Keeper, // Pilot, Bootstrap, LiteWorkload) // - Validating SPIFFE IDs against expected patterns for each component type // - Supporting multiple trust domains via environment configuration // - Handling both exact matches and extended IDs with instance metadata // - Peer authorization for inter-component communication // // SPIFFE ID Construction: // // Each SPIKE component has a standardized SPIFFE ID pattern. Functions are // provided to construct these IDs: // // nexusID := spiffeid.Nexus("example.org") // // Returns: "spiffe://example.org/spike/nexus" // // pilotID := spiffeid.Pilot("example.org") // // Returns: "spiffe://example.org/spike/pilot/role/superuser" // // Identity Validation: // // Validation functions check if a given SPIFFE ID matches the expected pattern // for a component type, supporting both exact and extended matches: // // if spiffeid.IsNexus("spiffe://example.org/spike/nexus") { // // Handle Nexus-specific logic // } // // // Extended IDs with metadata are also recognized // if spiffeid.IsNexus("spiffe://example.org/spike/nexus/instance-0") { // // Also recognized as Nexus // } // // Supported Components: // - Nexus: The central secrets management service // - Keeper: Distributed key storage component // - Pilot: Administrative/superuser role // - Bootstrap: Initial setup component // - LiteWorkload: Workloads with limited encryption-only access // // Multi-Trust Domain Support: // // All validation functions support multiple trust domains configured via // environment variables, allowing SPIKE deployments across different trust // boundaries. package spiffeid spike-sdk-go-0.16.4/spiffeid/spiffeid.go000066400000000000000000000121231511163700700200100ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package spiffeid import ( "path" "strings" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // Keeper constructs and returns the SPIKE Keeper's SPIFFE ID string. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/keeper" func Keeper(trustRoot string) string { const fName = "Keeper" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "keeper") } // Nexus constructs and returns the SPIFFE ID for SPIKE Nexus. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/nexus" func Nexus(trustRoot string) string { const fName = "Nexus" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "nexus") } // Pilot generates the SPIFFE ID for a SPIKE Pilot superuser role. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/pilot/role/superuser" func Pilot(trustRoot string) string { const fName = "Pilot" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "superuser") } // Bootstrap generates the SPIFFE ID for a SPIKE Bootstrap role. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/bootstrap" func Bootstrap(trustRoot string) string { const fName = "Bootstrap" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "bootstrap") } // LiteWorkload generates the SPIFFE ID for a SPIKE Lite workload role. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/workload/role/lite" func LiteWorkload(trustRoot string) string { const fName = "LiteWorkload" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "workload", "role", "lite") } // PilotRecover generates the SPIFFE ID for a SPIKE Pilot recovery role. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/pilot/role/recover" func PilotRecover(trustRoot string) string { const fName = "PilotRecover" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "recover") } // PilotRestore generates the SPIFFE ID for a SPIKE Pilot restore role. // // Parameters: // - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is // obtained from the environment. // // Returns: // - string: The complete SPIFFE ID in the format: // "spiffe:///spike/pilot/role/restore" func PilotRestore(trustRoot string) string { const fName = "PilotRestore" if trustRoot == "" { failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain log.FatalErr(fName, *failErr) } if strings.Contains(trustRoot, ",") { failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains log.FatalErr(fName, *failErr) } return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "restore") } spike-sdk-go-0.16.4/spiffeid/spiffeid_test.go000066400000000000000000000360711511163700700210570ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package spiffeid import ( "os" "strings" "testing" "github.com/stretchr/testify/assert" ) // TestKeeper_ValidTrustRoot tests Keeper SPIFFE ID construction func TestKeeper_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/keeper", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/keeper", }, { name: "LocalDomain", trustRoot: "local.domain", expected: "spiffe://local.domain/spike/keeper", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Keeper(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/keeper") }) } } // TestNexus_ValidTrustRoot tests Nexus SPIFFE ID construction func TestNexus_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/nexus", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/nexus", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Nexus(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/nexus") }) } } // TestPilot_ValidTrustRoot tests Pilot SPIFFE ID construction func TestPilot_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/pilot/role/superuser", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/pilot/role/superuser", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Pilot(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/pilot/role/superuser") }) } } // TestBootstrap_ValidTrustRoot tests Bootstrap SPIFFE ID construction func TestBootstrap_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/bootstrap", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/bootstrap", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Bootstrap(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/bootstrap") }) } } // TestLiteWorkload_ValidTrustRoot tests LiteWorkload SPIFFE ID construction func TestLiteWorkload_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/workload/role/lite", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/workload/role/lite", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := LiteWorkload(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/workload/role/lite") }) } } // TestPilotRecover_ValidTrustRoot tests PilotRecover SPIFFE ID construction func TestPilotRecover_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/pilot/role/recover", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/pilot/role/recover", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := PilotRecover(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/pilot/role/recover") }) } } // TestPilotRestore_ValidTrustRoot tests PilotRestore SPIFFE ID construction func TestPilotRestore_ValidTrustRoot(t *testing.T) { tests := []struct { name string trustRoot string expected string }{ { name: "SimpleDomain", trustRoot: "example.org", expected: "spiffe://example.org/spike/pilot/role/restore", }, { name: "SubDomain", trustRoot: "trust.example.org", expected: "spiffe://trust.example.org/spike/pilot/role/restore", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := PilotRestore(tt.trustRoot) assert.Equal(t, tt.expected, result) assert.True(t, strings.HasPrefix(result, "spiffe://")) assert.Contains(t, result, "/spike/pilot/role/restore") }) } } // TestIsPilot_ExactMatch tests IsPilot with exact match func TestIsPilot_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_PILOT", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_PILOT") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/pilot/role/superuser", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/pilot/role/superuser/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/pilot/role/superuser", expected: false, }, { name: "PartialMatch", spiffeID: "spiffe://example.org/spike/keeper", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsPilot(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsLiteWorkload_ExactMatch tests IsLiteWorkload with exact match func TestIsLiteWorkload_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_LITE_WORKLOAD", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_LITE_WORKLOAD") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/workload/role/lite", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/workload/role/lite/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/workload/role/lite", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsLiteWorkload(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsPilotRecover_ExactMatch tests IsPilotRecover with exact match func TestIsPilotRecover_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_PILOT", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_PILOT") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/pilot/role/recover", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/pilot/role/recover/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/pilot/role/recover", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsPilotRecover(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsPilotRestore_ExactMatch tests IsPilotRestore with exact match func TestIsPilotRestore_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_PILOT", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_PILOT") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/pilot/role/restore", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/pilot/role/restore/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/pilot/role/restore", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsPilotRestore(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsBootstrap_ExactMatch tests IsBootstrap with exact match func TestIsBootstrap_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_BOOTSTRAP", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_BOOTSTRAP") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/bootstrap", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/bootstrap/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/bootstrap", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsBootstrap(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsKeeper_ExactMatch tests IsKeeper with exact match func TestIsKeeper_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_KEEPER", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_KEEPER") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/keeper", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/keeper/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/keeper", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsKeeper(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestIsNexus_ExactMatch tests IsNexus with exact match func TestIsNexus_ExactMatch(t *testing.T) { // Set up environment variable os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") tests := []struct { name string spiffeID string expected bool }{ { name: "ExactMatch", spiffeID: "spiffe://example.org/spike/nexus", expected: true, }, { name: "ExtendedMatch", spiffeID: "spiffe://example.org/spike/nexus/instance-0", expected: true, }, { name: "NoMatch", spiffeID: "spiffe://other.org/spike/nexus", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsNexus(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestPeerCanTalkToAnyone tests the debug function func TestPeerCanTalkToAnyone(t *testing.T) { tests := []struct { name string peer1 string peer2 string }{ {"BothEmpty", "", ""}, {"OneEmpty", "spiffe://example.org/spike/nexus", ""}, {"BothValid", "spiffe://example.org/spike/nexus", "spiffe://example.org/spike/keeper"}, {"Invalid", "not-a-spiffe-id", "also-not-a-spiffe-id"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := PeerCanTalkToAnyone(tt.peer1, tt.peer2) assert.True(t, result, "PeerCanTalkToAnyone should always return true") }) } } // TestPeerCanTalkToKeeper tests keeper authorization func TestPeerCanTalkToKeeper(t *testing.T) { // Set up environment variables os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org") os.Setenv("SPIKE_TRUST_ROOT_BOOTSTRAP", "example.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") defer os.Unsetenv("SPIKE_TRUST_ROOT_BOOTSTRAP") tests := []struct { name string peerID string expected bool }{ { name: "NexusCanTalk", peerID: "spiffe://example.org/spike/nexus", expected: true, }, { name: "BootstrapCanTalk", peerID: "spiffe://example.org/spike/bootstrap", expected: true, }, { name: "NexusExtendedCanTalk", peerID: "spiffe://example.org/spike/nexus/instance-0", expected: true, }, { name: "BootstrapExtendedCanTalk", peerID: "spiffe://example.org/spike/bootstrap/instance-0", expected: true, }, { name: "PilotCannotTalk", peerID: "spiffe://example.org/spike/pilot/role/superuser", expected: false, }, { name: "KeeperCannotTalk", peerID: "spiffe://example.org/spike/keeper", expected: false, }, { name: "InvalidCannotTalk", peerID: "spiffe://other.org/spike/nexus", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := PeerCanTalkToKeeper(tt.peerID) assert.Equal(t, tt.expected, result) }) } } // TestMultipleTrustDomains tests validation with multiple trust domains func TestMultipleTrustDomains(t *testing.T) { // Set up environment variable with multiple trust domains os.Setenv("SPIKE_TRUST_ROOT_NEXUS", "example.org, other.org") defer os.Unsetenv("SPIKE_TRUST_ROOT_NEXUS") tests := []struct { name string spiffeID string expected bool }{ { name: "FirstDomain", spiffeID: "spiffe://example.org/spike/nexus", expected: true, }, { name: "SecondDomain", spiffeID: "spiffe://other.org/spike/nexus", expected: true, }, { name: "ThirdDomainNotConfigured", spiffeID: "spiffe://third.org/spike/nexus", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsNexus(tt.spiffeID) assert.Equal(t, tt.expected, result) }) } } // TestSPIFFEIDFormat tests the format of generated SPIFFE IDs func TestSPIFFEIDFormat(t *testing.T) { trustRoot := "example.org" ids := map[string]string{ "Keeper": Keeper(trustRoot), "Nexus": Nexus(trustRoot), "Pilot": Pilot(trustRoot), "Bootstrap": Bootstrap(trustRoot), "LiteWorkload": LiteWorkload(trustRoot), "PilotRecover": PilotRecover(trustRoot), "PilotRestore": PilotRestore(trustRoot), } for name, id := range ids { t.Run(name, func(t *testing.T) { // All IDs should start with spiffe:// assert.True(t, strings.HasPrefix(id, "spiffe://"), "%s ID should start with spiffe://", name) // All IDs should contain the trust root assert.Contains(t, id, trustRoot, "%s ID should contain trust root", name) // All IDs should contain /spike/ assert.Contains(t, id, "/spike/", "%s ID should contain /spike/", name) // ID should not contain double slashes except in spiffe:// cleaned := strings.Replace(id, "spiffe://", "", 1) assert.False(t, strings.Contains(cleaned, "//"), "%s ID should not contain double slashes", name) }) } } spike-sdk-go-0.16.4/strings/000077500000000000000000000000001511163700700155715ustar00rootroot00000000000000spike-sdk-go-0.16.4/strings/char.go000066400000000000000000000145261511163700700170450ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package strings import ( "crypto/rand" sdkErrors "github.com/spiffe/spike-sdk-go/errors" "github.com/spiffe/spike-sdk-go/log" ) // secureRandomStringFromCharClass generates a cryptographically secure random // string of the specified length using characters from the given character // class. // // # Security: Fatal Exit on CSPRNG Failure // // This function uses crypto/rand.Read() as its source of randomness. If the // cryptographic random number generator fails, this function will terminate // the program with log.FatalErr() rather than returning an error. // // This design decision is intentional and critical for security: // // 1. CSPRNG failures indicate fundamental system compromise or misconfiguration // 2. This function generates security-sensitive strings (passwords, tokens, // API keys, secrets) where weak randomness would be catastrophic // 3. Silently falling back to weaker randomness or continuing execution would // create a false sense of security // 4. A CSPRNG failure is an exceptional, unrecoverable system-level error // (kernel entropy depletion, hardware failure, or system compromise) // 5. Consistent with other security-critical operations in the SDK (Shamir // secret sharing, SVID acquisition) that also fatal exit on failure // // DO NOT remove this fatal exit behavior. Allowing the function to return // an error that could be ignored would compromise the security guarantees // of all code using this function. // // Parameters: // - charClass: character class specification supporting: // - Predefined classes: \w (word chars), \d (digits), \x (symbols) // - Custom ranges: "A-Z", "a-z", "0-9", or combinations like "A-Za-z0-9" // - Individual characters: any literal characters // - length: number of characters in the resulting string // // Returns: // - string: the generated random string, empty on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrStringEmptyCharacterClass: if character class is empty // - ErrStringInvalidRange: if character range is invalid // - ErrStringEmptyCharacterSet: if character set is empty // // Note: CSPRNG failures (crypto/rand.Read) cause immediate program termination // via log.FatalErr() for security reasons (cannot generate secure random data). // This is intentional and critical - DO NOT remove this fatal exit behavior. func secureRandomStringFromCharClass( charClass string, length int, ) (string, *sdkErrors.SDKError) { const fName = "secureRandomStringFromCharClass" chars, err := expandCharacterClass(charClass) if err != nil { return "", err } if len(chars) == 0 { failErr := sdkErrors.ErrStringEmptyCharacterSet failErr.Msg = "character class resulted in empty character set" return "", failErr } result := make([]byte, length) randomBytes := make([]byte, length) if _, randErr := rand.Read(randomBytes); randErr != nil { failErr := sdkErrors.ErrCryptoRandomGenerationFailed.Wrap(randErr) failErr.Msg = "cryptographic random number generator failed" log.FatalErr(fName, *failErr) } for i := 0; i < length; i++ { result[i] = chars[randomBytes[i]%byte(len(chars))] } return string(result), nil } // expandCharacterClass expands character class expressions into a string // containing all valid characters from the class. It handles both predefined // character classes and custom character ranges. // // Parameters: // - charClass: character class expression supporting: // - Predefined classes: // - \w: word characters (a-z, A-Z, 0-9, and underscore) // - \d: digits (0-9) // - \x: symbols (printable ASCII excluding letters and digits) // - Custom ranges: // - Single characters: included as-is // - Range notation: "A-Z" expands to all uppercase letters // - Combined ranges: "A-Za-z0-9" expands to alphanumeric characters // // Returns: // - string: expanded character set containing all characters from the class, // empty on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrStringEmptyCharacterClass: if character class is empty // - ErrStringInvalidRange: if character range is invalid (e.g., "Z-A") // - ErrStringEmptyCharacterSet: if expansion results in empty set func expandCharacterClass(charClass string) (string, *sdkErrors.SDKError) { // Check for empty character class first if len(charClass) == 0 { failErr := sdkErrors.ErrStringEmptyCharacterClass failErr.Msg = "character class cannot be empty" return "", failErr } charSet := make(map[byte]bool) // Use map to avoid duplicates // Handle predefined character classes switch charClass { case "\\w": // Word characters: letters, digits, underscore for c := 'a'; c <= 'z'; c++ { charSet[byte(c)] = true } for c := 'A'; c <= 'Z'; c++ { charSet[byte(c)] = true } for c := '0'; c <= '9'; c++ { charSet[byte(c)] = true } charSet['_'] = true case "\\d": // Digits for c := '0'; c <= '9'; c++ { charSet[byte(c)] = true } case "\\x": // Symbols (printable ASCII excluding letters and digits) for c := 32; c <= 126; c++ { ch := byte(c) if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) { charSet[ch] = true } } default: // Handle character ranges and individual characters like A-Za-z0-9 i := 0 for i < len(charClass) { if i+2 < len(charClass) && charClass[i+1] == '-' { // Range specification start := charClass[i] end := charClass[i+2] // Only allow forward ranges (`start <= end`) if start > end { failErr := sdkErrors.ErrStringInvalidRange failErr.Msg = "invalid character range: start > end" return "", failErr } // Add all characters in range for c := start; c <= end; c++ { charSet[c] = true } i += 3 } else { // Single character charSet[charClass[i]] = true i++ } } } // Convert map to slice chars := make([]byte, 0, len(charSet)) for char := range charSet { chars = append(chars, char) } // Final check for the empty result (this catches edge cases) if len(chars) == 0 { failErr := sdkErrors.ErrStringEmptyCharacterSet failErr.Msg = "character class resulted in empty character set" return "", failErr } return string(chars), nil } spike-sdk-go-0.16.4/strings/char_test.go000066400000000000000000000271201511163700700200760ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package strings import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestExpandCharacterClass_PredefinedClasses tests predefined character classes func TestExpandCharacterClass_PredefinedClasses(t *testing.T) { tests := []struct { name string charClass string expectedChars map[byte]bool }{ { name: "WordClass", charClass: "\\w", expectedChars: func() map[byte]bool { m := make(map[byte]bool) for c := 'a'; c <= 'z'; c++ { m[byte(c)] = true } for c := 'A'; c <= 'Z'; c++ { m[byte(c)] = true } for c := '0'; c <= '9'; c++ { m[byte(c)] = true } m['_'] = true return m }(), }, { name: "DigitClass", charClass: "\\d", expectedChars: func() map[byte]bool { m := make(map[byte]bool) for c := '0'; c <= '9'; c++ { m[byte(c)] = true } return m }(), }, { name: "SymbolClass", charClass: "\\x", expectedChars: func() map[byte]bool { m := make(map[byte]bool) for c := 32; c <= 126; c++ { ch := byte(c) if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) { m[ch] = true } } return m }(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) assert.Nil(t, err) assert.NotEmpty(t, result) // Verify all expected characters are present for expectedChar := range tt.expectedChars { assert.Contains(t, result, string(expectedChar), "Expected character %c (%d) to be in result", expectedChar, expectedChar) } // Verify no unexpected characters for _, c := range result { assert.True(t, tt.expectedChars[byte(c)], "Unexpected character %c (%d) in result", c, c) } }) } } // TestExpandCharacterClass_CustomRanges tests custom character ranges func TestExpandCharacterClass_CustomRanges(t *testing.T) { tests := []struct { name string charClass string expectedLength int expectedStart byte expectedEnd byte }{ {"UppercaseRange", "A-Z", 26, 'A', 'Z'}, {"LowercaseRange", "a-z", 26, 'a', 'z'}, {"DigitRange", "0-9", 10, '0', '9'}, {"SmallRange", "A-C", 3, 'A', 'C'}, {"SingleCharRange", "A-A", 1, 'A', 'A'}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) assert.Nil(t, err) assert.Equal(t, tt.expectedLength, len(result)) // Verify range for c := tt.expectedStart; c <= tt.expectedEnd; c++ { assert.Contains(t, result, string(c)) } }) } } // TestExpandCharacterClass_CombinedRanges tests combined character ranges func TestExpandCharacterClass_CombinedRanges(t *testing.T) { tests := []struct { name string charClass string expectedLength int }{ {"AlphanumericLower", "a-z0-9", 36}, {"AlphanumericUpper", "A-Z0-9", 36}, {"AlphanumericBoth", "A-Za-z0-9", 62}, {"MultipleRanges", "A-C0-2", 6}, // ABC012 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) assert.Nil(t, err) assert.Equal(t, tt.expectedLength, len(result)) }) } } // TestExpandCharacterClass_SingleCharacters tests individual characters func TestExpandCharacterClass_SingleCharacters(t *testing.T) { tests := []struct { name string charClass string expected string }{ {"SingleChar", "A", "A"}, {"MultipleChars", "ABC", "ABC"}, {"MixedCharsAndRange", "XA-CY", "XABCY"}, {"SpecialChars", "!@#", "!@#"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) assert.Nil(t, err) assert.Equal(t, len(tt.expected), len(result)) // Check all expected characters are present for _, c := range tt.expected { assert.Contains(t, result, string(c)) } }) } } // TestExpandCharacterClass_EmptyCharClass tests empty character class func TestExpandCharacterClass_EmptyCharClass(t *testing.T) { result, err := expandCharacterClass("") assert.Empty(t, result) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrStringEmptyCharacterClass)) } // TestExpandCharacterClass_InvalidRange tests invalid character ranges func TestExpandCharacterClass_InvalidRange(t *testing.T) { tests := []struct { name string charClass string }{ {"BackwardRange", "Z-A"}, {"BackwardDigitRange", "9-0"}, {"BackwardLowerRange", "z-a"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) assert.Empty(t, result) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrStringInvalidRange)) }) } } // TestExpandCharacterClass_OverlappingRanges tests overlapping ranges func TestExpandCharacterClass_OverlappingRanges(t *testing.T) { // Overlapping ranges should not produce duplicates result, err := expandCharacterClass("A-CA-C") assert.Nil(t, err) // Should have only 3 unique characters: A, B, C assert.Equal(t, 3, len(result)) assert.Contains(t, result, "A") assert.Contains(t, result, "B") assert.Contains(t, result, "C") } // TestExpandCharacterClass_EdgeCases tests edge cases func TestExpandCharacterClass_EdgeCases(t *testing.T) { tests := []struct { name string charClass string wantErr bool }{ {"DashAtEnd", "ABC-", false}, // Dash at end is treated as literal {"DashAtStart", "-ABC", false}, // Dash at start is treated as literal {"OnlyDash", "-", false}, // Single dash is treated as literal {"MultipleDashes", "---", false}, // Multiple dashes treated as literals } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := expandCharacterClass(tt.charClass) if tt.wantErr { assert.NotNil(t, err) assert.Empty(t, result) } else { assert.Nil(t, err) assert.NotEmpty(t, result) } }) } } // TestSecureRandomStringFromCharClass_ValidClasses tests valid character classes func TestSecureRandomStringFromCharClass_ValidClasses(t *testing.T) { tests := []struct { name string charClass string length int }{ {"WordClass10", "\\w", 10}, {"DigitClass10", "\\d", 10}, {"SymbolClass10", "\\x", 10}, {"UppercaseRange", "A-Z", 20}, {"LowercaseRange", "a-z", 20}, {"Alphanumeric", "A-Za-z0-9", 30}, {"SingleChar", "A", 5}, {"SpecialChars", "!@#$%", 15}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := secureRandomStringFromCharClass(tt.charClass, tt.length) assert.Nil(t, err) assert.Equal(t, tt.length, len(result)) // Verify all characters are from the expanded char class expandedChars, expandErr := expandCharacterClass(tt.charClass) require.Nil(t, expandErr) for _, c := range result { assert.Contains(t, expandedChars, string(c), "Character %c should be from character class %s", c, tt.charClass) } }) } } // TestSecureRandomStringFromCharClass_DifferentLengths tests different string lengths func TestSecureRandomStringFromCharClass_DifferentLengths(t *testing.T) { tests := []struct { name string length int }{ {"Length0", 0}, {"Length1", 1}, {"Length10", 10}, {"Length100", 100}, {"Length1000", 1000}, } charClass := "A-Za-z0-9" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := secureRandomStringFromCharClass(charClass, tt.length) assert.Nil(t, err) assert.Equal(t, tt.length, len(result)) }) } } // TestSecureRandomStringFromCharClass_EmptyCharClass tests empty character class func TestSecureRandomStringFromCharClass_EmptyCharClass(t *testing.T) { result, err := secureRandomStringFromCharClass("", 10) assert.Empty(t, result) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrStringEmptyCharacterClass)) } // TestSecureRandomStringFromCharClass_InvalidRange tests invalid character range func TestSecureRandomStringFromCharClass_InvalidRange(t *testing.T) { result, err := secureRandomStringFromCharClass("Z-A", 10) assert.Empty(t, result) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrStringInvalidRange)) } // TestSecureRandomStringFromCharClass_Randomness tests that multiple calls produce different results func TestSecureRandomStringFromCharClass_Randomness(t *testing.T) { charClass := "A-Za-z0-9" length := 20 // Generate multiple strings results := make(map[string]bool) iterations := 10 for i := 0; i < iterations; i++ { result, err := secureRandomStringFromCharClass(charClass, length) require.Nil(t, err) results[result] = true } // All results should be unique (extremely unlikely to have duplicates with 62^20 possibilities) assert.Equal(t, iterations, len(results), "Expected all generated strings to be unique") } // TestSecureRandomStringFromCharClass_CharacterDistribution tests character distribution func TestSecureRandomStringFromCharClass_CharacterDistribution(t *testing.T) { charClass := "ABC" length := 300 result, err := secureRandomStringFromCharClass(charClass, length) require.Nil(t, err) // Count occurrences of each character counts := make(map[rune]int) for _, c := range result { counts[c]++ } // With 3 characters and 300 iterations, we expect roughly 100 of each // Allow for statistical variation (at least 50 of each) assert.GreaterOrEqual(t, counts['A'], 50, "Character A should appear at least 50 times") assert.GreaterOrEqual(t, counts['B'], 50, "Character B should appear at least 50 times") assert.GreaterOrEqual(t, counts['C'], 50, "Character C should appear at least 50 times") } // TestExpandCharacterClass_Consistency tests that same input produces same output func TestExpandCharacterClass_Consistency(t *testing.T) { charClass := "A-Za-z0-9" result1, err1 := expandCharacterClass(charClass) result2, err2 := expandCharacterClass(charClass) assert.Nil(t, err1) assert.Nil(t, err2) assert.Equal(t, len(result1), len(result2)) // Convert to maps for comparison (order doesn't matter) map1 := make(map[byte]bool) for _, c := range result1 { map1[byte(c)] = true } map2 := make(map[byte]bool) for _, c := range result2 { map2[byte(c)] = true } assert.Equal(t, map1, map2, "Same input should produce same character set") } // TestExpandCharacterClass_WordClassSize tests that \w has correct size func TestExpandCharacterClass_WordClassSize(t *testing.T) { result, err := expandCharacterClass("\\w") assert.Nil(t, err) // \w = a-z (26) + A-Z (26) + 0-9 (10) + _ (1) = 63 characters assert.Equal(t, 63, len(result)) } // TestExpandCharacterClass_DigitClassSize tests that \d has correct size func TestExpandCharacterClass_DigitClassSize(t *testing.T) { result, err := expandCharacterClass("\\d") assert.Nil(t, err) // \d = 0-9 = 10 characters assert.Equal(t, 10, len(result)) } // TestExpandCharacterClass_SymbolClassSize tests that \x has correct size func TestExpandCharacterClass_SymbolClassSize(t *testing.T) { result, err := expandCharacterClass("\\x") assert.Nil(t, err) // \x = printable ASCII (95) - letters (52) - digits (10) = 33 characters assert.Equal(t, 33, len(result)) } // TestSecureRandomStringFromCharClass_AllPredefinedClasses tests all predefined classes work func TestSecureRandomStringFromCharClass_AllPredefinedClasses(t *testing.T) { tests := []string{"\\w", "\\d", "\\x"} for _, charClass := range tests { t.Run(charClass, func(t *testing.T) { result, err := secureRandomStringFromCharClass(charClass, 20) assert.Nil(t, err) assert.Equal(t, 20, len(result)) }) } } spike-sdk-go-0.16.4/strings/template.go000066400000000000000000000147771511163700700177530ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package strings import ( "regexp" "strconv" stdstrings "strings" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // StringFromTemplate creates a string based on a template with embedded // generator expressions. // // # Template Syntax // // Generator expressions follow the pattern: [character_class]{length} // Where: // - character_class defines which characters can be generated // - length specifies how many characters to generate (must be a positive // integer) // // # Supported Character Classes // // ## Predefined Classes // // - \w : Word characters (a-z, A-Z, 0-9, _) // - \d : Digits (0-9) // - \x : Symbols (printable ASCII excluding letters and digits: // !"#$%&'()*+,-./:;<=>?@[\]^`{|}~ and space) // // ## Character Ranges // // - a-z : Lowercase letters // - A-Z : Uppercase letters // - 0-9 : Digits // - a-Z : All letters (equivalent to a-zA-Z) // // ## Multiple Ranges and Characters // // You can combine multiple ranges and individual characters within a // single class: // - [a-zA-Z0-9] : Letters and digits // - [A-Za-z0-6] : Letters and digits 0-6 // - [0-9a-fA-F] : Hexadecimal characters // - [A-Ca-c1-3] : A,B,C,a,b,c,1,2,3 // // Individual characters can be mixed with ranges: // - [a-z_.-] : Lowercase letters plus underscore, period, and hyphen // - [A-Z0-9!@#] : Uppercase letters, digits, and specific symbols // // # Template Examples // // StringFromTemplate("user[0-9]{4}") // "user1234" // StringFromTemplate("pass[a-zA-Z0-9]{12}") // "passA3kL9mX2nQ8z" // StringFromTemplate("prefix[\w]{8}suffix") // "prefixaB3_kM9Zsuffix" // StringFromTemplate("id[0-9a-f]{8}-[0-9a-f]{4}") // "a1b2c3d4-ef56" // StringFromTemplate("admin[a-z]{3}[A-Z]{2}[0-9]{3}") // "adminxyzAB123" // // # Error Conditions // // The function returns an error for: // - Invalid ranges where start > end: [z-a] or [9-0] // - Empty character classes: [] // - Invalid length specifications: non-numeric values // - Malformed expressions: missing brackets or braces // // # Implementation Notes // // Character ranges are inclusive on both ends. When multiple ranges overlap // (e.g., [a-zA-Z] contains both a-z and A-Z), duplicate characters are // automatically deduplicated. // // Ranges must follow ASCII ordering. Cross-case ranges like [a-Z] work because // they span the ASCII range from 'a' (97) to 'Z' (90), but this includes // punctuation characters between uppercase and lowercase letters. // // # Limitations and Assumptions // // This implementation assumes reasonable usage patterns: // - Character classes should be logically organized // - Ranges should follow natural ordering (a-z, not z-a) // - Individual characters mixed with ranges are supported but should be // used judiciously // - Unicode characters beyond ASCII are not explicitly supported // - Escape sequences beyond \w, \d, \x are not supported // - Character class negation (^) is not supported // - POSIX character classes ([:alpha:], [:digit:]) are not supported // // The function prioritizes common use cases for password generation, API keys, // tokens, and identifiers while maintaining simplicity and predictability. // // # Security: Fatal Exit on CSPRNG Failure // // This function uses crypto/rand.Read() for generating random characters. If the // cryptographic random number generator fails, this function will terminate the // program with log.FatalErr() rather than returning an error. // // This design decision is intentional and critical for security: // // 1. CSPRNG failures indicate fundamental system compromise or misconfiguration // 2. This function is used for generating security-sensitive strings (passwords, // tokens, API keys, secrets) where weak randomness would be catastrophic // 3. Silently falling back to weaker randomness or returning an error that could // be ignored would create a false sense of security // 4. A CSPRNG failure is an exceptional, unrecoverable system-level error // // DO NOT modify this behavior to return errors for CSPRNG failures, as it would // compromise the security guarantees of all code using this function. // // # Parameters // // template: A string containing literal text and generator expressions. // // Generator expressions are replaced with random characters. // // # Returns // // Returns: // - string: The generated string with all generator expressions replaced, // empty on error // - *sdkErrors.SDKError: nil on success, or one of the following errors: // - ErrStringInvalidLength: if length specification is not a valid number // - ErrStringNegativeLength: if length is negative // - ErrStringEmptyCharacterClass: if character class is empty // - ErrStringInvalidRange: if character range is invalid // - ErrStringEmptyCharacterSet: if character set is empty // // Note: CSPRNG failures (crypto/rand.Read) cause immediate program termination // via log.FatalErr() for security reasons (cannot generate secure random data). // This is intentional and critical - see "Security: Fatal Exit on CSPRNG Failure" // section above for rationale. func StringFromTemplate(template string) (string, *sdkErrors.SDKError) { // Regular expression to match generator expressions like [a-z]{5} or [\w]{3} // Modified to capture any content in braces, not just digits // Changed + to * to allow empty character classes like [] re := regexp.MustCompile(`\[([^]]*)]\{([^}]+)}`) result := template // Find all matches and replace them for { match := re.FindStringSubmatch(result) if match == nil { break } fullMatch := match[0] charClass := match[1] lengthStr := match[2] // Parse length - this will now catch non-numeric values length, parseErr := strconv.Atoi(lengthStr) if parseErr != nil { failErr := sdkErrors.ErrStringInvalidLength.Wrap(parseErr) failErr.Msg = "invalid length specification in template" return "", failErr } // Validate that length is non-negative if length < 0 { failErr := sdkErrors.ErrStringNegativeLength failErr.Msg = "length cannot be negative in template" return "", failErr } // Generate random string based on character class randomStr, err := secureRandomStringFromCharClass(charClass, length) if err != nil { return "", err } // Replace the first occurrence of the pattern result = stdstrings.Replace(result, fullMatch, randomStr, 1) } return result, nil } spike-sdk-go-0.16.4/strings/template_test.go000066400000000000000000000323421511163700700207760ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package strings import ( "regexp" stdstrings "strings" "testing" ) //nolint:gocyclo func TestStringFromTemplate(t *testing.T) { tests := []struct { name string template string expectError bool validate func(t *testing.T, result string) // Custom validation function }{ { name: "simple word characters", template: `football[\w]{8}bartender`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "football") { t.Errorf("result should start with 'football', got: %s", result) } if !stdstrings.HasSuffix(result, "bartender") { t.Errorf("result should end with 'bartender', got: %s", result) } // Extract the middle part and check it's 8 word characters middle := result[8 : len(result)-9] // football(8chars)bartender if len(middle) != 8 { t.Errorf("middle part should be 8 characters, got %d: %s", len(middle), middle) } matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]{8}$`, middle) if !matched { t.Errorf("middle part should contain only word characters, got: %s", middle) } }, }, { name: "lowercase letters and digits", template: `admin[a-z0-9]{3}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "admin") { t.Errorf("result should start with 'admin', got: %s", result) } suffix := result[5:] // Everything after "admin" if len(suffix) != 3 { t.Errorf("suffix should be 3 characters, got %d: %s", len(suffix), suffix) } matched, _ := regexp.MatchString(`^[a-z0-9]{3}$`, suffix) if !matched { t.Errorf("suffix should contain only lowercase letters and digits, got: %s", suffix) } }, }, { name: "multiple patterns", template: `admin[a-z0-9]{3}something[\w]{3}`, validate: func(t *testing.T, result string) { // Should be: admin + 3 chars + something + 3 chars expectedLen := 5 + 3 + 9 + 3 // admin(5) + random(3) + something(9) + random(3) if len(result) != expectedLen { t.Errorf("result should be %d characters, got %d: %s", expectedLen, len(result), result) } if !stdstrings.HasPrefix(result, "admin") { t.Errorf("result should start with 'admin', got: %s", result) } if !stdstrings.Contains(result, "something") { t.Errorf("result should contain 'something', got: %s", result) } }, }, { name: "letters and digits mixed", template: `pass[a-zA-Z0-9]{12}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "pass") { t.Errorf("result should start with 'pass', got: %s", result) } suffix := result[4:] if len(suffix) != 12 { t.Errorf("suffix should be 12 characters, got %d: %s", len(suffix), suffix) } matched, _ := regexp.MatchString(`^[a-zA-Z0-9]{12}$`, suffix) if !matched { t.Errorf("suffix should contain only letters and digits, got: %s", suffix) } }, }, { name: "cross-case range a-Z", template: `fail[a-Z]{8}`, expectError: true, }, { name: "mixed case letters", template: `pass[A-Za-z]{8}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "pass") { t.Errorf("result should start with 'pass', got: %s", result) } suffix := result[4:] if len(suffix) != 8 { t.Errorf("suffix should be 8 characters, got %d: %s", len(suffix), suffix) } matched, _ := regexp.MatchString(`^[A-Za-z]{8}$`, suffix) if !matched { t.Errorf("suffix should contain only letters, got: %s", suffix) } }, }, { name: "digits only", template: `football[\d]{8}bartender`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "football") { t.Errorf("result should start with 'football', got: %s", result) } if !stdstrings.HasSuffix(result, "bartender") { t.Errorf("result should end with 'bartender', got: %s", result) } middle := result[8 : len(result)-9] if len(middle) != 8 { t.Errorf("middle part should be 8 characters, got %d: %s", len(middle), middle) } matched, _ := regexp.MatchString(`^[0-9]{8}$`, middle) if !matched { t.Errorf("middle part should contain only digits, got: %s", middle) } }, }, { name: "symbols only", template: `football[\x]{4}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "football") { t.Errorf("result should start with 'football', got: %s", result) } suffix := result[8:] if len(suffix) != 4 { t.Errorf("suffix should be 4 characters, got %d: %s", len(suffix), suffix) } // \x should be symbols (printable ASCII excluding letters and digits) for _, char := range suffix { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') { t.Errorf("suffix should contain only symbols, but found alphanumeric: %c in %s", char, suffix) } } }, }, { name: "multiple different patterns", template: `user[A-Z]{2}[0-9]{4}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "user") { t.Errorf("result should start with 'user', got: %s", result) } suffix := result[4:] if len(suffix) != 6 { // 2 uppercase + 4 digits t.Errorf("suffix should be 6 characters, got %d: %s", len(suffix), suffix) } // The first 2 should be uppercase letters upperPart := suffix[:2] matched, _ := regexp.MatchString(`^[A-Z]{2}$`, upperPart) if !matched { t.Errorf("first part should be uppercase letters, got: %s", upperPart) } // The last 4 should be digits digitPart := suffix[2:] matched, _ = regexp.MatchString(`^[0-9]{4}$`, digitPart) if !matched { t.Errorf("second part should be digits, got: %s", digitPart) } }, }, { name: "no patterns", template: `simple`, validate: func(t *testing.T, result string) { if result != "simple" { t.Errorf("result should be 'simple', got: %s", result) } }, }, { name: "pattern in middle", template: `prefix[\w]{10}suffix`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "prefix") { t.Errorf("result should start with 'prefix', got: %s", result) } if !stdstrings.HasSuffix(result, "suffix") { t.Errorf("result should end with 'suffix', got: %s", result) } middle := result[6 : len(result)-6] // prefix(6chars)suffix(6chars) if len(middle) != 10 { t.Errorf("middle part should be 10 characters, got %d: %s", len(middle), middle) } }, }, { name: "hex characters", template: `test[0-9a-fA-F]{8}`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "test") { t.Errorf("result should start with 'test', got: %s", result) } suffix := result[4:] if len(suffix) != 8 { t.Errorf("suffix should be 8 characters, got %d: %s", len(suffix), suffix) } matched, _ := regexp.MatchString(`^[0-9a-fA-F]{8}$`, suffix) if !matched { t.Errorf("suffix should contain only hex characters, got: %s", suffix) } }, }, { name: "multiple small ranges", template: `prefix[A-Ca-c1-3]{5}suffix`, validate: func(t *testing.T, result string) { if !stdstrings.HasPrefix(result, "prefix") { t.Errorf("result should start with 'prefix', got: %s", result) } if !stdstrings.HasSuffix(result, "suffix") { t.Errorf("result should end with 'suffix', got: %s", result) } middle := result[6 : len(result)-6] if len(middle) != 5 { t.Errorf("middle part should be 5 characters, got %d: %s", len(middle), middle) } // Should only contain A, B, C, a, b, c, 1, 2, 3. matched, _ := regexp.MatchString(`^[ABCabc123]{5}$`, middle) if !matched { t.Errorf("middle part should contain only A,B,C,a,b,c,1,2,3, got: %s", middle) } }, }, // Error cases { name: "invalid range z-a", template: `pass[z-a]{8}`, expectError: true, }, { name: "empty character class", template: `[]{5}`, expectError: true, }, { name: "zero length", template: `[a-z]{0}`, validate: func(t *testing.T, result string) { if result != "" { t.Errorf("result should be empty string for zero length, got: %s", result) } }, }, { name: "invalid length - non-numeric", template: `[a-z]{abc}`, expectError: true, }, { name: "malformed - missing closing bracket", template: `[a-z{5}`, validate: func(t *testing.T, result string) { // This should be treated as literal text since it doesn't match the pattern if result != "[a-z{5}" { t.Errorf("malformed pattern should be treated as literal, got: %s", result) } }, }, { name: "malformed - missing opening brace", template: `[a-z]5}`, validate: func(t *testing.T, result string) { // This should be treated as literal text since it doesn't match the pattern if result != "[a-z]5}" { t.Errorf("malformed pattern should be treated as literal, got: %s", result) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := StringFromTemplate(tt.template) if tt.expectError { if err == nil { t.Errorf("expected error but got none, result: %s", result) } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if tt.validate != nil { tt.validate(t, result) } }) } } // TestStringFromTemplateConsistency tests that the function produces different results // on multiple calls (since it should be generating random strings) func TestStringFromTemplateConsistency(t *testing.T) { template := `test[a-zA-Z0-9]{10}` results := make(map[string]bool) iterations := 100 for i := 0; i < iterations; i++ { result, err := StringFromTemplate(template) if err != nil { t.Fatalf("unexpected error on iteration %d: %v", i, err) } if !stdstrings.HasPrefix(result, "test") { t.Errorf("result should start with 'test', got: %s", result) } if len(result) != 14 { // "test" + 10 random chars t.Errorf("result should be 14 characters, got %d: %s", len(result), result) } results[result] = true } // We should have generated many different strings // With 10 random alphanumeric characters, the chance of collision is very low if len(results) < iterations/2 { t.Errorf("expected more unique results, got %d unique out of %d iterations", len(results), iterations) } } // TestStringFromTemplateLength tests various length specifications func TestStringFromTemplateLength(t *testing.T) { testCases := []struct { template string expectedLen int }{ {`[a-z]{1}`, 1}, {`[a-z]{5}`, 5}, {`[a-z]{20}`, 20}, {`prefix[a-z]{10}suffix`, 22}, // 6 + 10 + 6 {`[a-z]{5}[A-Z]{3}[0-9]{2}`, 10}, // 5 + 3 + 2 } for _, tc := range testCases { t.Run(tc.template, func(t *testing.T) { result, err := StringFromTemplate(tc.template) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(result) != tc.expectedLen { t.Errorf("expected length %d, got %d: %s", tc.expectedLen, len(result), result) } }) } } // BenchmarkStringFromTemplate benchmarks the function performance func BenchmarkStringFromTemplate(b *testing.B) { template := `user[a-zA-Z0-9]{16}[A-Z]{4}[0-9]{8}` b.ResetTimer() for i := 0; i < b.N; i++ { _, err := StringFromTemplate(template) if err != nil { b.Fatalf("unexpected error: %v", err) } } } // TestStringFromTemplateEdgeCases tests edge cases and boundary conditions func TestStringFromTemplateEdgeCases(t *testing.T) { tests := []struct { name string template string expectError bool validate func(t *testing.T, result string) }{ { name: "empty template", template: "", validate: func(t *testing.T, result string) { if result != "" { t.Errorf("empty template should return empty string, got: %s", result) } }, }, { name: "single character class", template: `[a]{1}`, validate: func(t *testing.T, result string) { if result != "a" { t.Errorf("single character class should return that character, got: %s", result) } }, }, { name: "large length", template: `[a]{100}`, validate: func(t *testing.T, result string) { if len(result) != 100 { t.Errorf("result should be 100 characters, got %d", len(result)) } for _, char := range result { if char != 'a' { t.Errorf("all characters should be 'a', found: %c", char) } } }, }, { name: "consecutive patterns", template: `[a]{2}[b]{2}[c]{2}`, validate: func(t *testing.T, result string) { expected := "aabbcc" if result != expected { t.Errorf("expected %s, got %s", expected, result) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := StringFromTemplate(tt.template) if tt.expectError { if err == nil { t.Errorf("expected error but got none, result: %s", result) } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if tt.validate != nil { tt.validate(t, result) } }) } } spike-sdk-go-0.16.4/system/000077500000000000000000000000001511163700700154245ustar00rootroot00000000000000spike-sdk-go-0.16.4/system/doc.go000066400000000000000000000025171511163700700165250ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package system provides utilities for application lifecycle management // and graceful shutdown handling. // // The package includes functionality for: // - Graceful shutdown on OS signals (SIGINT, SIGTERM) // - Condition-based watching with polling and exit actions // - Application keep-alive for long-running services // // Signal Handling: // // KeepAlive blocks until the application receives a termination signal, // enabling graceful shutdown: // // func main() { // setupApp() // defer cleanup() // system.KeepAlive() // } // // Condition Watching: // // Watch continuously polls a condition and executes an action when the // condition becomes true, useful for initialization monitoring: // // config := system.WatchConfig{ // WaitTimeBeforeExit: 5 * time.Second, // PollInterval: 1 * time.Second, // InitializationPredicate: func() bool { // return isServiceReady() // }, // ExitAction: func() { // log.Println("Service ready") // }, // } // go system.Watch(config) // // This package is typically used in main functions to control application // lifecycle and ensure proper cleanup during shutdown. package system spike-sdk-go-0.16.4/system/system.go000066400000000000000000000106501511163700700173010ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package system import ( "os" "os/signal" "syscall" "time" ) // KeepAlive blocks the current goroutine until it receives either a // SIGINT (Ctrl+C) or SIGTERM signal, enabling graceful shutdown of the // application. // // The function creates a buffered channel to handle OS signals and uses // signal.Notify to register for SIGINT and SIGTERM signals. It then blocks // until a signal is received. // // An optional callback can be provided to handle the received signal. If no // callback is provided, no action is taken when a signal is received (the // function simply returns). This allows callers to handle logging, cleanup, // or other actions as needed. // // This is typically used in the main function to prevent the program from // exiting immediately and to ensure proper cleanup when the program is // terminated. // // Parameters: // - onSignal: Optional callback invoked when a signal is received, with the // signal as parameter. If not provided, the function returns silently. // // Example usage: // // func main() { // // Initialize your application // setupApp() // // // Keep the application running until shutdown signal // KeepAlive(func(sig os.Signal) { // log.Printf("Received %v signal, shutting down gracefully...\n", sig) // }) // // // Perform cleanup // cleanup() // } // // Example without callback: // // func main() { // setupApp() // KeepAlive() // Simply blocks until signal, no logging // cleanup() // } func KeepAlive(onSignal ...func(os.Signal)) { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) sig := <-sigChan if len(onSignal) > 0 && onSignal[0] != nil { onSignal[0](sig) } } // WatchConfig defines the configuration for the Watch function. type WatchConfig struct { // WaitTimeBeforeExit specifies how long to wait after initialization // before executing the exit action. WaitTimeBeforeExit time.Duration // PollInterval defines how frequently to check the initialization predicate. PollInterval time.Duration // InitializationPredicate is a function that returns true when the watched // condition is met and initialization is complete. InitializationPredicate func() bool // ExitAction is the function to execute after the initialization predicate // returns true and the wait time has elapsed. ExitAction func() // OnTick is an optional callback invoked on each polling interval. // If nil, no action is taken on tick. OnTick func() // OnInitialized is an optional callback invoked when the initialization // predicate returns true, before waiting and executing the exit action. // If nil, no action is taken on initialization. OnInitialized func() } // Watch continuously polls a condition at regular intervals and executes an // exit action once the condition is met. It will poll using the // InitializationPredicate function at intervals specified by PollInterval. // When the predicate returns true, it invokes the OnInitialized callback (if // provided), waits for WaitTimeBeforeExit duration, and then executes // ExitAction. // // The OnTick callback (if provided) is invoked on each polling interval before // checking the initialization predicate. The OnInitialized callback (if // provided) // is invoked when the predicate first returns true. // // This function runs indefinitely until the exit action is called, so it // should typically be run in a goroutine if the exit action doesn't terminate // the program. // // Example: // // config := WatchConfig{ // WaitTimeBeforeExit: 5 * time.Second, // PollInterval: 1 * time.Second, // InitializationPredicate: func() bool { // return isServiceReady() // }, // OnTick: func() { // log.Println("Checking service status...") // }, // OnInitialized: func() { // log.Println("Service initialized successfully") // }, // ExitAction: func() { // log.Println("Shutting down watcher") // os.Exit(0) // }, // } // go Watch(config) func Watch(config WatchConfig) { interval := config.PollInterval ticker := time.NewTicker(interval) for range ticker.C { if config.OnTick != nil { config.OnTick() } if config.InitializationPredicate() { if config.OnInitialized != nil { config.OnInitialized() } time.Sleep(config.WaitTimeBeforeExit) config.ExitAction() } } } spike-sdk-go-0.16.4/validation/000077500000000000000000000000001511163700700162325ustar00rootroot00000000000000spike-sdk-go-0.16.4/validation/doc.go000066400000000000000000000035751511163700700173400ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 // Package validation provides input validation utilities for SPIKE API // operations. // // The package includes validation functions for: // - Names: alphanumeric strings with length and format constraints // - SPIFFE IDs: both exact IDs and regex patterns for identity matching // - Paths: resource paths with support for wildcards and special characters // - Path patterns: regex patterns for path matching in policies // - Policy IDs: UUID format validation // - Permissions: verification against allowed permission types // // All validation functions return errors.ErrInvalidInput when validation fails. // // Name Validation: // // Names must be 1-250 characters and contain only alphanumeric characters, // hyphens, underscores, and spaces: // // if err := validation.ValidateName("my-policy-name"); err != nil { // log.Fatal("Invalid name") // } // // SPIFFE ID Validation: // // Validates both raw SPIFFE IDs and regex patterns: // // // Raw SPIFFE ID // err := validation.ValidateSPIFFEID("spiffe://example.org/service/api") // // // SPIFFE ID pattern with wildcards // err = validation.ValidateSPIFFEIDPattern("^spiffe://example.org/.*$") // // Path Validation: // // Validates resource paths and path patterns: // // // Simple path // err := validation.ValidatePath("app/secrets/database") // // // Path pattern with wildcards // err = validation.ValidatePathPattern("app/.*/database") // // Permission Validation: // // Verifies that permissions are in the allowed set: // // permissions := []data.PolicyPermission{ // data.PermissionRead, // data.PermissionWrite, // } // if err := validation.ValidatePermissions(permissions); err != nil { // log.Fatal("Invalid permissions") // } package validation spike-sdk-go-0.16.4/validation/validation.go000066400000000000000000000157541511163700700207270ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package validation import ( "regexp" "github.com/google/uuid" "github.com/spiffe/spike-sdk-go/api/entity/data" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) const validNamePattern = `^[a-zA-Z0-9-_ ]+$` const maxNameLength = 250 const validSPIFFEIDPattern = `^\^?spiffe://[\\a-zA-Z0-9.\-*()+?\[\]]+(/[\\/a-zA-Z0-9._\-*()+?\[\]]+)*\$?$` const validRawSPIFFEIDPattern = `^spiffe://[a-zA-Z0-9.-]+(/[a-zA-Z0-9._-]+)*$` const maxPathPatternLength = 500 const validPathPattern = `^[a-zA-Z0-9._\-/^$()?+*|[\]{}\\]+$` const validPath = `^[a-zA-Z0-9._\-/()?+*|[\]{}\\]+$` // ValidateName checks if the provided name meets length and format constraints. // // The name must be between 1 and 250 characters and contain only alphanumeric // characters, hyphens, underscores, and spaces. // // Parameters: // - name: The name string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if name is empty, exceeds 250 characters, or // contains invalid characters func ValidateName(name string) *sdkErrors.SDKError { // Validate length if len(name) == 0 || len(name) > maxNameLength { return sdkErrors.ErrDataInvalidInput } // Validate format if match, _ := regexp.MatchString(validNamePattern, name); !match { return sdkErrors.ErrDataInvalidInput } return nil } // ValidateSPIFFEIDPattern validates whether the given SPIFFE ID pattern string // conforms to the expected format. // // The pattern may include regex special characters for matching multiple // SPIFFE IDs. // It must start with "spiffe://" and follow the SPIFFE ID specification with // optional regex metacharacters. // // Parameters: // - SPIFFEIDPattern: The SPIFFE ID pattern string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if the pattern does not conform to the expected // format func ValidateSPIFFEIDPattern(SPIFFEIDPattern string) *sdkErrors.SDKError { // Validate SPIFFEIDPattern if match, _ := regexp.MatchString( validSPIFFEIDPattern, SPIFFEIDPattern); !match { return sdkErrors.ErrDataInvalidInput } return nil } // ValidateSPIFFEID validates if the given SPIFFE ID matches the expected // format. // // Unlike ValidateSPIFFEIDPattern, this function validates raw SPIFFE IDs // without regex metacharacters. The ID must strictly conform to the SPIFFE // specification: // "spiffe:///". // // Parameters: // - SPIFFEID: The SPIFFE ID string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if the SPIFFE ID does not conform to the expected // format func ValidateSPIFFEID(SPIFFEID string) *sdkErrors.SDKError { if match, _ := regexp.MatchString( validRawSPIFFEIDPattern, SPIFFEID); !match { return sdkErrors.ErrDataInvalidInput } return nil } // ValidatePathPattern validates the given path pattern string for correctness. // // This function is used for validating path patterns that may contain regex // metacharacters for matching multiple paths. The path pattern must be between // 1 and 500 characters and may include regex anchors (^, $) and other regex // special characters (?, +, *, |, [], {}, \, etc.) along with alphanumeric // characters, underscores, hyphens, forward slashes, and periods. // // Use ValidatePath instead if you need to validate literal paths without // regex anchors. // // Parameters: // - pathPattern: The path pattern string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if the pattern is empty, exceeds 500 characters, or // contains invalid characters func ValidatePathPattern(pathPattern string) *sdkErrors.SDKError { // Validate length if len(pathPattern) == 0 || len(pathPattern) > maxPathPatternLength { return sdkErrors.ErrDataInvalidInput } // Validate format // Allow regex special characters along with alphanumeric and basic symbols if match, _ := regexp.MatchString(validPathPattern, pathPattern); !match { return sdkErrors.ErrDataInvalidInput } return nil } // ValidatePath checks if the given path is valid based on predefined rules. // // This function validates paths that should not contain regex anchor // metacharacters (^ or $). Unlike ValidatePathPattern, this function is for // validating literal paths. The path must be between 1 and 500 characters. // // Note: While this function excludes regex anchors (^, $), it still allows // other special characters that may appear in actual paths such as ?, +, *, // |, [], {}, \, /, etc. Use ValidatePathPattern if you need to validate // patterns that include regex anchors. // // Parameters: // - path: The path string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if the path is empty, exceeds 500 characters, or // contains invalid characters func ValidatePath(path string) *sdkErrors.SDKError { if len(path) == 0 || len(path) > maxPathPatternLength { return sdkErrors.ErrDataInvalidInput } if match, _ := regexp.MatchString(validPath, path); !match { return sdkErrors.ErrDataInvalidInput } return nil } // ValidatePolicyID verifies if the given policy ID is a valid UUID format. // // The policy ID must conform to the UUID specification (RFC 4122). // This function uses the google/uuid package for validation. // // Parameters: // - policyID: The policy ID string to validate // // Returns: // - *sdkErrors.SDKError: nil if valid, or one of the following errors: // - ErrDataInvalidInput: if the policy ID is not a valid UUID func ValidatePolicyID(policyID string) *sdkErrors.SDKError { err := uuid.Validate(policyID) if err != nil { return sdkErrors.ErrDataInvalidInput } return nil } // ValidatePermissions checks if all provided permissions are valid. // // Permissions are compared against a predefined list of allowed permissions: // - PermissionList: list secrets // - PermissionRead: read secret values // - PermissionWrite: create/update secrets // - PermissionSuper: administrative access // // Parameters: // - permissions: Slice of policy permissions to validate // // Returns: // - *sdkErrors.SDKError: nil if all permissions are valid, or one of the // following errors: // - ErrDataInvalidInput: if any permission is not in the allowed list func ValidatePermissions( permissions []data.PolicyPermission, ) *sdkErrors.SDKError { allowedPermissions := []data.PolicyPermission{ data.PermissionList, data.PermissionRead, data.PermissionWrite, data.PermissionSuper, } for _, permission := range permissions { isAllowed := false for _, allowedPermission := range allowedPermissions { if permission == allowedPermission { isAllowed = true break } } if !isAllowed { return sdkErrors.ErrDataInvalidInput } } return nil } spike-sdk-go-0.16.4/validation/validation_test.go000066400000000000000000000326431511163700700217620ustar00rootroot00000000000000// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 package validation import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/spiffe/spike-sdk-go/api/entity/data" sdkErrors "github.com/spiffe/spike-sdk-go/errors" ) // TestValidateName_Valid tests ValidateName with valid names func TestValidateName_Valid(t *testing.T) { tests := []struct { name string input string }{ {"Simple", "simple-name"}, {"WithUnderscore", "name_with_underscore"}, {"WithSpace", "name with space"}, {"Alphanumeric", "Name123"}, {"Mixed", "My-Policy_Name 123"}, {"SingleChar", "a"}, {"MaxLength", strings.Repeat("a", 250)}, {"AllNumbers", "12345"}, {"AllDashes", "----"}, {"AllUnderscores", "____"}, {"AllSpaces", " "}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateName(tt.input) assert.Nil(t, err, "Expected valid name: %s", tt.input) }) } } // TestValidateName_Invalid tests ValidateName with invalid names func TestValidateName_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"TooLong", strings.Repeat("a", 251)}, {"WithSlash", "name/with/slash"}, {"WithDot", "name.with.dot"}, {"WithSpecialChars", "name@example"}, {"WithParentheses", "name(test)"}, {"WithBrackets", "name[test]"}, {"WithBraces", "name{test}"}, {"WithAsterisk", "name*"}, {"WithQuestion", "name?"}, {"WithPlus", "name+"}, {"WithEquals", "name=value"}, {"WithPipe", "name|other"}, {"WithBackslash", "name\\test"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateName(tt.input) assert.NotNil(t, err, "Expected invalid name: %s", tt.input) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidateSPIFFEIDPattern_Valid tests ValidateSPIFFEIDPattern with valid patterns func TestValidateSPIFFEIDPattern_Valid(t *testing.T) { tests := []struct { name string input string }{ {"Simple", "spiffe://example.org/service"}, {"WithAnchor", "^spiffe://example.org/service$"}, {"WithWildcard", "spiffe://example.org/.*"}, {"WithPlus", "spiffe://example.org/service.+"}, {"WithQuestion", "spiffe://example.org/service.?"}, {"WithCharClass", "spiffe://example.org/[a-z]+"}, {"WithUnderscore", "spiffe://example.org/service_name"}, {"WithDot", "spiffe://trust.example.org/service"}, {"WithDash", "spiffe://example-org.com/service"}, {"MultiplePath", "spiffe://example.org/path/to/service"}, {"WithNumbers", "spiffe://example.org/service123"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateSPIFFEIDPattern(tt.input) assert.Nil(t, err, "Expected valid SPIFFE ID pattern: %s", tt.input) }) } } // TestValidateSPIFFEIDPattern_Invalid tests ValidateSPIFFEIDPattern with invalid patterns func TestValidateSPIFFEIDPattern_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"NoScheme", "example.org/service"}, {"WrongScheme", "http://example.org/service"}, {"NoTrustDomain", "spiffe:///service"}, {"TrailingSlash", "spiffe://example.org/"}, {"Spaces", "spiffe://example.org/service name"}, {"InvalidChars", "spiffe://example.org/service@test"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateSPIFFEIDPattern(tt.input) assert.NotNil(t, err, "Expected invalid SPIFFE ID pattern: %s", tt.input) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidateSPIFFEID_Valid tests ValidateSPIFFEID with valid IDs func TestValidateSPIFFEID_Valid(t *testing.T) { tests := []struct { name string input string }{ {"Simple", "spiffe://example.org/service"}, {"WithUnderscore", "spiffe://example.org/service_name"}, {"WithDot", "spiffe://trust.example.org/service.api"}, {"WithDash", "spiffe://example-org.com/service-name"}, {"MultiplePath", "spiffe://example.org/path/to/service"}, {"WithNumbers", "spiffe://example.org/service123"}, {"Subdomain", "spiffe://sub.example.org/workload"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateSPIFFEID(tt.input) assert.Nil(t, err, "Expected valid SPIFFE ID: %s", tt.input) }) } } // TestValidateSPIFFEID_Invalid tests ValidateSPIFFEID with invalid IDs func TestValidateSPIFFEID_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"NoScheme", "example.org/service"}, {"WrongScheme", "http://example.org/service"}, {"NoTrustDomain", "spiffe:///service"}, {"WithAnchor", "^spiffe://example.org/service$"}, {"WithWildcard", "spiffe://example.org/.*"}, {"WithRegex", "spiffe://example.org/service.+"}, {"WithCharClass", "spiffe://example.org/[a-z]"}, {"Spaces", "spiffe://example.org/service name"}, {"SpecialChars", "spiffe://example.org/service@test"}, {"Parentheses", "spiffe://example.org/(service)"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateSPIFFEID(tt.input) assert.NotNil(t, err, "Expected invalid SPIFFE ID: %s", tt.input) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidatePathPattern_Valid tests ValidatePathPattern with valid patterns func TestValidatePathPattern_Valid(t *testing.T) { tests := []struct { name string input string }{ {"SimplePath", "app/secrets"}, {"WithWildcard", "app/.*/secrets"}, {"WithPlus", "app/secret.+"}, {"WithQuestion", "app/secret.?"}, {"WithCharClass", "app/[a-z]+"}, {"WithParens", "app/(prod|dev)/secrets"}, {"WithUnderscore", "app/secret_name"}, {"WithDot", "app/secret.json"}, {"WithDash", "app/secret-name"}, {"WithAnchor", "^app/secrets$"}, {"WithPipe", "app|config"}, {"WithBraces", "app/{id}"}, {"WithBrackets", "app[0-9]"}, {"MaxLength", strings.Repeat("a", 500)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePathPattern(tt.input) assert.Nil(t, err, "Expected valid path pattern: %s", tt.input) }) } } // TestValidatePathPattern_Invalid tests ValidatePathPattern with invalid patterns func TestValidatePathPattern_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"TooLong", strings.Repeat("a", 501)}, {"WithSpace", "app/secret name"}, {"WithAt", "app/@secret"}, {"WithHash", "app/#secret"}, {"WithPercent", "app/%secret"}, {"WithAmpersand", "app/&secret"}, {"WithEquals", "app/secret=value"}, {"WithColon", "app/secret:value"}, {"WithSemicolon", "app/secret;value"}, {"WithComma", "app/secret,value"}, {"WithLessThan", "app/secret"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePathPattern(tt.input) assert.NotNil(t, err, "Expected invalid path pattern: %s", tt.input) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidatePath_Valid tests ValidatePath with valid paths func TestValidatePath_Valid(t *testing.T) { tests := []struct { name string input string }{ {"SimplePath", "app/secrets"}, {"WithUnderscore", "app/secret_name"}, {"WithDot", "app/secret.json"}, {"WithDash", "app/secret-name"}, {"Nested", "app/prod/database/credentials"}, {"WithNumbers", "app/secret123"}, {"SingleLevel", "secrets"}, {"WithParentheses", "app/(prod)/secrets"}, {"WithBrackets", "app/[index]/secrets"}, {"WithBraces", "app/{id}/secrets"}, {"WithPipe", "app|config"}, {"WithBackslash", "app\\path"}, {"WithPlus", "app+secrets"}, {"WithAsterisk", "app*secrets"}, {"WithQuestion", "app?secrets"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePath(tt.input) assert.Nil(t, err, "Expected valid path: %s", tt.input) }) } } // TestValidatePath_Invalid tests ValidatePath with invalid paths func TestValidatePath_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"TooLong", strings.Repeat("a", 501)}, {"WithSpace", "app/secret name"}, {"WithAt", "app/@secret"}, {"WithHash", "app/#secret"}, {"WithPercent", "app/%secret"}, {"WithAmpersand", "app/&secret"}, {"WithEquals", "app/secret=value"}, {"WithColon", "app/secret:value"}, {"WithSemicolon", "app/secret;value"}, {"WithComma", "app/secret,value"}, {"WithLessThan", "app/secret"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePath(tt.input) assert.NotNil(t, err, "Expected invalid path: %s", tt.input) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidatePolicyID_Valid tests ValidatePolicyID with valid UUIDs func TestValidatePolicyID_Valid(t *testing.T) { tests := []struct { name string input string }{ {"UUID_v4", "550e8400-e29b-41d4-a716-446655440000"}, {"UUID_v1", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"}, {"UUID_v5", "886313e1-3b8a-5372-9b90-0c9aee199e5d"}, {"AllZeros", "00000000-0000-0000-0000-000000000000"}, {"AllOnes", "11111111-1111-1111-1111-111111111111"}, {"MixedCase", "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"}, {"Lowercase", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePolicyID(tt.input) assert.Nil(t, err, "Expected valid policy ID: %s", tt.input) }) } } // TestValidatePolicyID_Invalid tests ValidatePolicyID with invalid UUIDs func TestValidatePolicyID_Invalid(t *testing.T) { tests := []struct { name string input string }{ {"Empty", ""}, {"TooShort", "550e8400-e29b-41d4-a716"}, {"TooLong", "550e8400-e29b-41d4-a716-446655440000-extra"}, {"WrongFormat", "550e8400-e29b-41d4-a716-44665544000"}, {"InvalidChars", "550e8400-e29b-41d4-a716-44665544000g"}, {"Spaces", "550e8400 e29b 41d4 a716 446655440000"}, {"MissingSegment", "550e8400--41d4-a716-446655440000"}, {"NotUUID", "not-a-uuid"}, {"JustHyphens", "----"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePolicyID(tt.input) if err == nil { t.Fatalf("Expected invalid policy ID: %s, but got nil error", tt.input) } assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidatePermissions_Valid tests ValidatePermissions with valid permissions func TestValidatePermissions_Valid(t *testing.T) { tests := []struct { name string input []data.PolicyPermission }{ {"SingleRead", []data.PolicyPermission{data.PermissionRead}}, {"SingleWrite", []data.PolicyPermission{data.PermissionWrite}}, {"SingleList", []data.PolicyPermission{data.PermissionList}}, {"SingleSuper", []data.PolicyPermission{data.PermissionSuper}}, {"ReadWrite", []data.PolicyPermission{data.PermissionRead, data.PermissionWrite}}, {"AllPermissions", []data.PolicyPermission{ data.PermissionRead, data.PermissionWrite, data.PermissionList, data.PermissionSuper, }}, {"Empty", []data.PolicyPermission{}}, {"Duplicates", []data.PolicyPermission{ data.PermissionRead, data.PermissionRead, }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePermissions(tt.input) assert.Nil(t, err, "Expected valid permissions") }) } } // TestValidatePermissions_Invalid tests ValidatePermissions with invalid permissions func TestValidatePermissions_Invalid(t *testing.T) { tests := []struct { name string input []data.PolicyPermission }{ {"Invalid", []data.PolicyPermission{"invalid"}}, {"MixedWithInvalid", []data.PolicyPermission{ data.PermissionRead, "invalid", }}, {"UnknownPermission", []data.PolicyPermission{"execute"}}, {"Typo", []data.PolicyPermission{"reed"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePermissions(tt.input) assert.NotNil(t, err, "Expected invalid permissions") assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) }) } } // TestValidateName_BoundaryConditions tests boundary conditions for name validation func TestValidateName_BoundaryConditions(t *testing.T) { // Test boundary at 250 characters exactly250 := strings.Repeat("a", 250) err := ValidateName(exactly250) assert.Nil(t, err) // Test boundary at 251 characters exactly251 := strings.Repeat("a", 251) err = ValidateName(exactly251) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) } // TestValidatePathPattern_BoundaryConditions tests boundary conditions for path pattern validation func TestValidatePathPattern_BoundaryConditions(t *testing.T) { // Test boundary at 500 characters exactly500 := strings.Repeat("a", 500) err := ValidatePathPattern(exactly500) assert.Nil(t, err) // Test boundary at 501 characters exactly501 := strings.Repeat("a", 501) err = ValidatePathPattern(exactly501) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) } // TestValidatePath_BoundaryConditions tests boundary conditions for path validation func TestValidatePath_BoundaryConditions(t *testing.T) { // Test boundary at 500 characters exactly500 := strings.Repeat("a", 500) err := ValidatePath(exactly500) assert.Nil(t, err) // Test boundary at 501 characters exactly501 := strings.Repeat("a", 501) err = ValidatePath(exactly501) assert.NotNil(t, err) assert.True(t, err.Is(sdkErrors.ErrDataInvalidInput)) } // TestValidationConstants tests that constants are defined correctly func TestValidationConstants(t *testing.T) { assert.Equal(t, 250, maxNameLength) assert.Equal(t, 500, maxPathPatternLength) }