Compare commits

...

111 Commits
v0.1.8 ... main

Author SHA1 Message Date
Volkan Özçelik e7312f630a
update SPIFFE ID matchers.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-05-28 22:26:59 -07:00
Volkan Özçelik 3ec883d85a
update SPIFFE ID matchers.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-05-20 22:17:03 -07:00
Volkan Özçelik 6f50ebcc3b
Merge pull request #29 from spiffe/feature/trust-roots
Ability to query multiple trust roots in validators
2025-05-04 15:12:28 -07:00
Volkan Özçelik 7943a77e76
Version bump.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-05-04 15:10:35 -07:00
Volkan Özçelik fa7c888c1e
Validators accept multiple trust roots.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-05-04 15:09:57 -07:00
Volkan Özçelik 73f9090a5c
Merge pull request #28 from spiffe/feature/trust-root
Add trustRoot as an argument for flexibility.
2025-05-03 20:43:49 -07:00
Volkan Özçelik 571d7a5c49
Add trustRoot as an argument for flexibility.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-05-03 20:41:27 -07:00
Volkan Özçelik 6da43b2e92
Merge pull request #27 from marikann/fix-transport-config
Enhance HTTP client configuration with timeout and connection settings
2025-04-14 09:12:52 -07:00
muhammet.arikan a13ecac1c0
Enhance HTTP client configuration with timeout and connection settings
Signed-off-by: muhammet.arikan <muhammetarikann@hotmail.com>
2025-04-14 18:11:18 +03:00
Volkan Özçelik bbf51edf17
Add memory locking.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-04-10 21:00:44 -07:00
Volkan Özçelik 9da8df0d37
Merge pull request #26 from spiffe/feature/mlock
Add memory locking.
2025-04-10 20:54:33 -07:00
Volkan Özçelik 052208495d
Add memory locking.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-04-10 20:43:34 -07:00
Volkan Özçelik bc5f5deafa
Add hack folder.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-04-08 22:40:02 -07:00
Volkan Özçelik 80803711cd
Merge pull request #25 from spiffe/feature/wip
Add new methods.
2025-04-08 22:32:39 -07:00
Volkan Özçelik 6879e44a62
Remove doc.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-04-08 22:32:14 -07:00
Volkan Özçelik 22f368c29c
Add new methods.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-04-08 07:36:10 -07:00
Volkan Özçelik fe4b7df59e
BREAKING: Signature change in mem.*
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-30 15:41:38 -07:00
Volkan Özçelik ed499a14d8
Minor changes.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-29 20:56:57 -07:00
Volkan Özçelik 5a94c7e59f
Minor changes.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-29 20:50:38 -07:00
Volkan Özçelik 1df225b23d
Merge pull request #24 from spiffe/feature/paranoid-android
Add ClearParanoid
2025-03-29 15:11:02 -07:00
Volkan Özçelik 5627fc6f30
Add ClearParanoid
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-29 14:59:42 -07:00
Volkan Özçelik 75d43543c1
Minor API change
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-29 12:38:08 -07:00
Volkan Özçelik e8284b82bc
remove keeper id from request
keepers should not know info about themselves (including their ids)

Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-07 19:41:06 -08:00
Volkan Özçelik 053e17ca09
convert sensitive data to pointer types
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-07 15:32:50 -08:00
Volkan Özçelik 4bacaf4b5f
Merge pull request #23 from spiffe/dependabot/go_modules/github.com/go-jose/go-jose/v4-4.0.5
Bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5
2025-03-07 13:05:52 -08:00
dependabot[bot] 627bef8961
Bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 20:52:52 +00:00
Volkan Özçelik 2d73c0cea4
Merge pull request #22 from spiffe/dependabot/go_modules/golang.org/x/net-0.33.0
Bump golang.org/x/net from 0.28.0 to 0.33.0
2025-03-07 12:52:19 -08:00
dependabot[bot] b5fb33ad3c
Bump golang.org/x/net from 0.28.0 to 0.33.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.28.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.28.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 20:52:01 +00:00
Volkan Özçelik 1433709715
Merge pull request #21 from spiffe/dependabot/go_modules/golang.org/x/crypto-0.31.0
Bump golang.org/x/crypto from 0.26.0 to 0.31.0
2025-03-07 12:50:55 -08:00
dependabot[bot] bd50d02a9d
Bump golang.org/x/crypto from 0.26.0 to 0.31.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.26.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.26.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 20:50:27 +00:00
Volkan Özçelik 0a1ba6863d
Merge pull request #20 from spiffe/feature/id-fix
update recovery apis
2025-03-07 12:45:10 -08:00
Volkan Özçelik 50d077fec8
update recovery apis
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-07 12:35:40 -08:00
Volkan Özçelik 570854bd99
documentation update
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-06 21:51:58 -08:00
Volkan Özçelik 59174ca488
api signature update
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-06 19:12:51 -08:00
Volkan Özçelik d54056301a
update recover and restore methods
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-06 19:07:56 -08:00
Volkan Özçelik f7cb455150
Merge pull request #19 from spiffe/feature/datatype
Data type change
2025-03-06 18:04:55 -08:00
Volkan Özçelik 68f6ca21b4
Data type change
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-06 18:04:05 -08:00
Volkan Özçelik 44b9b3771c
Merge pull request #18 from spiffe/feature/shard
BREAKING: Change type of Shard
2025-03-06 15:48:40 -08:00
Volkan Özçelik 2329d7fee8
BREAKING: Change type of Shard
Change the data type from string to a byte array for better memory management.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-06 15:45:24 -08:00
Volkan Özçelik 26308b23eb
Merge pull request #17 from spiffe/feature/mem
add options to retry.Do
2025-03-05 13:55:15 -08:00
Volkan Özçelik 335763b4b4
add options to retry.Do
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-05 13:48:09 -08:00
Volkan Özçelik c1ebd57c90
Merge pull request #16 from spiffe/feature/mem
Secure memory management helpers
2025-03-05 12:54:30 -08:00
Volkan Özçelik b94f65e01d
Secure memory management helpers
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-05 12:52:40 -08:00
Volkan Özçelik a51ef1e36f
add import secrets method.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-03-01 15:44:42 -08:00
Volkan Özçelik 1862e854be
kv constructor signature change
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-28 22:12:26 -08:00
Volkan Özçelik 63c9ad76de
Merge pull request #15 from spiffe/feature/kv-generic
Make kv value more generic
2025-02-28 21:58:57 -08:00
Volkan Özçelik 58e8662189
Make kv value more generic
the entity was named `Secret`, renamed it to
`Value` as `Secret` was leaking business logic.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-28 21:58:15 -08:00
Volkan Özçelik 66a7676eb5
Merge pull request #14 from spiffe/feature/kv
Move key-value store out-of-tree
2025-02-28 21:46:28 -08:00
Volkan Özçelik 1feb7575eb
Move key-value store out-of-tree
Moved the in-memory key-value store out of tree into the SDK.

The module appears like a general-purpose key-value store, so
it can benefit others, too. As part of this SDK, consumers
can use the store without necessarily having to use the rest
of SPIKE.

In addition, this will provide transparency about how secrets
are stored in memory, and will enable creating isolated test
suites independent of the SPIKE core itself.

Also, those who want to integrate with SPIKE, or want to use
a similar backing store (maybe as a backup medium, or caching
purposes) will be able to use the very same implementation
that SPIKE uses internally eliminating possible compatibility
concerns.

In addition, if the key-value store is used for external
purposes by the community, it will mean additional hardening
and battle-testing and potential to recover edge cases and
performance improvements that we might have overlooked.

Overall, it feels like a good idea.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-28 19:55:44 -08:00
Volkan Özçelik cafe36b8f6
handle some more statuses
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-20 15:23:20 -08:00
Volkan Özçelik 2f90c1325f
added new errors
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 22:20:58 -08:00
Volkan Özçelik 1bef34085e
making spiffeid apis less stuttery
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 22:04:27 -08:00
Volkan Özçelik 110a67c829
add spiffeids.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 21:42:09 -08:00
Volkan Özçelik 5e28bf4f11
exported the constant
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 18:07:52 -08:00
Volkan Özçelik aeda8afdb6
api action const changes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 18:00:14 -08:00
Volkan Özçelik 249bfff41c
refactor url outside internal
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 17:39:13 -08:00
Volkan Özçelik decbd0c390
Add url constants
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 17:32:11 -08:00
Volkan Özçelik ff7712c564
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	api/entity/data/error_code.go
2025-02-16 16:57:57 -08:00
Volkan Özçelik 0cbe642783
Update README
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-16 16:55:20 -08:00
Volkan Özçelik 244915b014
Merge pull request #13 from spiffe/feature/doomsday
Implement restore and recover APIs
2025-02-08 13:19:36 -08:00
Volkan Özçelik b866ae7e9b
Implement restore and recover APIs
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-08 09:26:59 -08:00
Volkan Özçelik 49acd4c191
Merge pull request #12 from spiffe/feature/restore
Add entities for restore and recover requests
2025-02-07 21:32:30 -08:00
Volkan Özçelik cc83f688b3
Add entities for restore and recover requests
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-07 21:30:39 -08:00
Volkan Özçelik c0f0918512
update SPIFFE ID matcher regexp.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-03 19:38:19 -08:00
Volkan Özçelik 293190de44
Merge pull request #11 from spiffe/feature/spiffeidregex
update SPIFFE ID matcher regexp.
2025-02-03 19:29:45 -08:00
Volkan Özçelik 5b2663479f
update SPIFFE ID matcher regexp.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-03 19:28:57 -08:00
Volkan Özçelik 2b3aa80911
update SPIFFE ID matcher regexp.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-03 19:26:23 -08:00
Volkan Özçelik c3b9a9f981
update SPIFFE ID matcher regexp.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-02-03 19:06:01 -08:00
Volkan Özçelik 7476e82c2a
Merge pull request #10 from spiffe/feature/api-improvement
Added new packages to the SDK

Crossing fingers and landing.
2025-01-18 15:25:11 -08:00
Volkan Özçelik 8fd819d261
Added new packages to the SDK
Signed-off-by: Volkan Özçelik <me@volkan.io>
2025-01-18 15:20:37 -08:00
Volkan Özçelik 4e93332327
Merge pull request #9 from abhishek44sharma/main
#70: Removing unused version from ShardContributionRequest
2025-01-11 23:00:23 -08:00
Abhishek Sharma cc30d8fe5f Tyding mod files
Signed-off-by: Abhishek Sharma <abhishek44sharma@gmail.com>
2025-01-11 11:47:06 +05:30
Abhishek Sharma c0f910e50e #70: Removing unused version from ShardContributionRequest
Signed-off-by: Abhishek Sharma <abhishek44sharma@gmail.com>
2025-01-11 11:44:30 +05:30
Volkan Özçelik 59f882ecbb
BREAKING: Remove initialization check
Removing initialization checks since now SPIKE Nexus does not start until it fetches a root key from the keepers.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-27 03:43:29 -08:00
Volkan Özçelik 0d91ae5804
BREAKING: Remove initialization check
Removing initialization checks since now SPIKE Nexus does not start until it fetches a root key from the keepers.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-26 21:58:05 -08:00
Volkan Özçelik 0ef0af7351
BREAKING: Remove initialization check
Removing initialization checks since now SPIKE Nexus does not start until it fetches a root key from the keepers.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-26 21:46:59 -08:00
Volkan Özçelik 4afae14805
Merge pull request #8 from spiffe/feature/remove-check-init-state
BREAKING: Remove initialization check
2024-12-26 19:30:37 -08:00
Volkan Özçelik 625420f215
BREAKING: Remove initialization check
Removing initialization checks since now SPIKE Nexus does not start until it fetches a root key from the keepers.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-26 19:30:13 -08:00
Volkan Özçelik b98ec8ee62
add shard contribution request and response.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-26 09:46:40 -08:00
Volkan Özçelik 95488908fb
add shard contribution request and response.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-25 14:34:32 -08:00
Volkan Özçelik 5781a822b2
BREAKING: Update Serve method
net.Serve does not use predicates anymore since we have full policy control in action.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-24 08:50:55 -08:00
Volkan Özçelik aa0ec7f413
Add permission sanitization method.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 18:53:27 -08:00
Volkan Özçelik 035c0dd482
Remove redundant error codes.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 18:08:36 -08:00
Volkan Özçelik 201c41457c
Add new validation methods
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 16:05:28 -08:00
Volkan Özçelik 72410e9e65
Add new validation methods
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 15:50:39 -08:00
Volkan Özçelik b813778ec1
Add new validation methods
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 15:42:26 -08:00
Volkan Özçelik bed86e5836
add new error codes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 13:33:28 -08:00
Volkan Özçelik 5fc0273ed2
add new error codes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 12:33:09 -08:00
Volkan Özçelik 0bac4107a0
add new error codes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 12:24:41 -08:00
Volkan Özçelik b5ee191ad0
Merge pull request #7 from spiffe/feature/error-codes
add new error codes
2024-12-23 11:11:04 -08:00
Volkan Özçelik c9162e1867
add new error codes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 10:36:18 -08:00
Volkan Özçelik 866efed7b4
minor
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-23 09:18:08 -08:00
Volkan Özçelik 1fe2d43784
Merge pull request #6 from spiffe/feature/api-docs
Add some API documentation
2024-12-22 10:01:54 -08:00
Volkan Özçelik 68386cfe0c
Add some API documentation
Also renamed a method which might be a breaking change.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-22 10:00:38 -08:00
Volkan Özçelik 843d7c2944
Add checkinitstate
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-10 22:43:26 -08:00
Volkan Özçelik 128d88d2ea
Merge pull request #5 from spiffe/feature/fix-todos
Fixed TODOs inlined in the code
2024-12-10 22:36:57 -08:00
Volkan Özçelik d97a741aba
Fixed todos
Also some minor refactorings.

Some changes might be breaking.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-10 22:35:29 -08:00
Volkan Özçelik 4a6d012fb0
Fixed todos
Also some minor refactorings.

Some changes might be breaking.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-10 22:32:29 -08:00
Volkan Özçelik dd61ea4367
Fixed todos
Also some minor refactorings.

Some changes might be breaking.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-10 22:30:57 -08:00
Volkan Özçelik 0a462d13d7
minor
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-10 06:53:49 -08:00
Volkan Özçelik ccfdb6bc27
Update README
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 23:36:31 -08:00
Volkan Özçelik b63a22537c
BREAKING: API updates.
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 23:32:31 -08:00
Volkan Özçelik d89682d007
Merge pull request #4 from spiffe/feature/metadata
API updates.
2024-12-02 21:56:09 -08:00
Volkan Özçelik a7c30300d9
API updates.
* Created an API class
* Refactorings around certain methods.

Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 21:51:30 -08:00
Volkan Özçelik 338675da46
minor changes
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 12:55:48 -08:00
Volkan Özçelik ceaec294ce
Update README
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 12:45:13 -08:00
Volkan Özçelik e0c63a296a
more methods
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 12:30:52 -08:00
Volkan Özçelik 5de64b22ad
Merge pull request #3 from spiffe/feature/socket
add `EndpointSocket` helper
2024-12-02 12:23:25 -08:00
Volkan Özçelik 62296b93b1
add `EndpointSocket` helper
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 12:22:54 -08:00
Volkan Özçelik e4c83b032b
Merge pull request #2 from spiffe/feature/source
add `Source` helper to fetch SVID source
2024-12-02 12:04:05 -08:00
Volkan Özçelik d3924614f0
add `Source` helper to fetch SVID source
Signed-off-by: Volkan Özçelik <me@volkan.io>
2024-12-02 12:03:31 -08:00
74 changed files with 4697 additions and 688 deletions

View File

@ -23,6 +23,87 @@ Prerequisites:
2. `SPIFFE_ENDPOINT_SOCKET` environment variable set to address of the Workload
API (e.g. `unix:///tmp/agent.sock`).
// TODO: add more details and an example here.
## Usage Example
// TODO: add more documents COC, contributing, etc.
```go
package main
import (
"fmt"
spike "github.com/spiffe/spike-sdk-go/api"
)
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

View File

@ -1,62 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"encoding/json"
"errors"
"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/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
func CreatePolicy(source *workloadapi.X509Source,
name string, spiffeIdPattern string, pathPattern string,
permissions []data.PolicyPermission,
) error {
r := reqres.PolicyCreateRequest{
Name: name,
SpiffeIdPattern: spiffeIdPattern,
PathPattern: pathPattern,
Permissions: permissions,
}
mr, err := json.Marshal(r)
if err != nil {
return errors.Join(
errors.New("createPolicy: I am having problem generating the payload"),
err,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
if err != nil {
return err
}
body, err := net.Post(client, url.PolicyCreate(), mr)
if err != nil {
return err
}
res := reqres.PolicyCreateResponse{}
err = json.Unmarshal(body, &res)
if err != nil {
return errors.Join(
errors.New("createPolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return errors.New(string(res.Err))
}
return nil
}

View File

@ -1,55 +0,0 @@
package api
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
import (
"encoding/json"
"errors"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
func DeletePolicy(source *workloadapi.X509Source, id string) error {
r := reqres.PolicyDeleteRequest{
Id: id,
}
mr, err := json.Marshal(r)
if err != nil {
return errors.Join(
errors.New("deletePolicy: I am having problem generating the payload"),
err,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
if err != nil {
return err
}
body, err := net.Post(client, url.PolicyDelete(), mr)
if err != nil {
return err
}
res := reqres.PolicyDeleteResponse{}
err = json.Unmarshal(body, &res)
if err != nil {
return errors.Join(
errors.New("deletePolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return errors.New(string(res.Err))
}
return nil
}

View File

@ -1,57 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"encoding/json"
"errors"
"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/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
func GetPolicy(source *workloadapi.X509Source, id string) (*data.Policy, error) {
r := reqres.PolicyReadRequest{Id: id}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("getPolicy: I am having problem generating the payload"),
err,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.PolicyGet(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.PolicyReadResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("getPolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &res.Policy, nil
}

View File

@ -1,56 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"encoding/json"
"errors"
"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/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
func ListPolicies(source *workloadapi.X509Source) (*[]data.Policy, error) {
r := reqres.PolicyListRequest{}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("listPolicies: I am having problem generating the payload"),
err,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.PolicyList(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.PolicyListResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("getPolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &res.Policies, nil
}

373
api/api.go Normal file
View File

@ -0,0 +1,373 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/acl"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/operator"
"github.com/spiffe/spike-sdk-go/api/internal/impl/api/secret"
"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.
func New() *Api {
defaultEndpointSocket := spiffe.EndpointSocket()
source, _, err := spiffe.Source(context.Background(), defaultEndpointSocket)
if err != nil {
return nil
}
return &Api{source: source}
}
// NewWithSource initializes a new Api instance with the given X509Source.
func NewWithSource(source *workloadapi.X509Source) *Api {
return &Api{source: source}
}
// Close releases any resources held by the Api instance.
// It ensures proper cleanup of the underlying source.
func (a *Api) Close() {
spiffe.CloseSource(a.source)
}
// 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 the server.
//
// The function takes the following parameters:
// - name string: The name of the policy to be created
// - spiffeIdPattern string: The SPIFFE ID pattern that this policy will apply
// to
// - pathPattern string: The path pattern that this policy will match against
// - permissions []data.PolicyPermission: A slice of PolicyPermission defining
// the access rights for this policy
//
// The function returns an error if any of the following operations fail:
// - Marshaling the policy creation request
// - Creating the mTLS client
// - Making the HTTP POST request
// - Unmarshaling the response
// - Server-side policy creation (indicated in the response)
//
// Example usage:
//
// 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)
// return
// }
func (a *Api) CreatePolicy(
name string, spiffeIdPattern string, pathPattern string,
permissions []data.PolicyPermission,
) error {
return acl.CreatePolicy(a.source,
name, spiffeIdPattern, pathPattern, permissions)
}
// DeletePolicy removes an existing policy from the system using its name.
//
// The function takes the following parameters:
// - name string: The name of the policy to be deleted
//
// The function returns an error if any of the following operations fail:
// - Marshaling the policy deletion request
// - Creating the mTLS client
// - Making the HTTP POST request
// - Unmarshaling the response
// - Server-side policy deletion (indicated in the response)
//
// Example usage:
//
// err = api.DeletePolicy("doc-reader")
// if err != nil {
// log.Printf("Failed to delete policy: %v", err)
// return
// }
func (a *Api) DeletePolicy(name string) error {
return acl.DeletePolicy(a.source, name)
}
// GetPolicy retrieves a policy from the system using its name.
//
// The function takes the following parameters:
// - name string: The name of the policy to retrieve
//
// The function returns:
// - (*data.Policy, nil) if the policy is found
// - (nil, nil) if the policy is not found
// - (nil, error) if an error occurs during the operation
//
// Errors can occur during:
// - Marshaling the policy retrieval request
// - Creating the mTLS client
// - Making the HTTP POST request (except for not found cases)
// - Unmarshaling the response
// - Server-side policy retrieval (indicated in the response)
//
// Example usage:
//
// policy, err := api.GetPolicy("doc-reader")
// if err != nil {
// log.Printf("Error retrieving policy: %v", err)
// return
// }
// if policy == nil {
// log.Printf("Policy not found")
// return
// }
//
// log.Printf("Found policy: %+v", policy)
func (a *Api) GetPolicy(name string) (*data.Policy, error) {
return acl.GetPolicy(a.source, name)
}
// ListPolicies retrieves all policies from the system.
//
// The function returns:
// - (*[]data.Policy, nil) containing all policies if successful
// - (nil, nil) if no policies are found
// - (nil, error) if an error occurs during the operation
//
// Note: The returned slice pointer should be dereferenced before use:
//
// policies := *result
//
// Errors can occur during:
// - Marshaling the policy list request
// - Creating the mTLS client
// - Making the HTTP POST request (except for not found cases)
// - Unmarshaling the response
// - Server-side policy listing (indicated in the response)
//
// Example usage:
//
// result, err := api.ListPolicies()
// 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 (a *Api) ListPolicies() (*[]data.Policy, error) {
return acl.ListPolicies(a.source)
}
// DeleteSecretVersions deletes specified versions of a secret at the given
// path
//
// It constructs a delete request and sends it to the secrets API endpoint.
//
// Parameters:
// - path string: Path to the secret to delete
// - versions []int: Array of version numbers to delete
//
// Returns:
// - error: nil on success, unauthorized error if not logged in, or wrapped
// error on request/parsing failure
//
// Example:
//
// err := api.DeleteSecretVersions("secret/path", []int{1, 2})
func (a *Api) DeleteSecretVersions(path string, versions []int) error {
return secret.Delete(a.source, path, versions)
}
// DeleteSecret deletes the entire secret at the given path
//
// Parameters:
// - path string: Path to the secret to delete
//
// Returns:
// - error: nil on success, unauthorized error if not logged in, or wrapped
// error on request/parsing failure
//
// Example:
//
// err := api.DeleteSecret("secret/path")
func (a *Api) DeleteSecret(path string) error {
return secret.Delete(a.source, path, []int{})
}
// GetSecretVersion retrieves a specific version of a secret at the given
// path.
//
// Parameters:
// - path string: Path to the secret to retrieve
// - version int: Version number of the secret to retrieve
//
// Returns:
// - *data.Secret: Secret data if found, nil if secret not found
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// secret, err := api.GetSecretVersion("secret/path", 1)
func (a *Api) GetSecretVersion(
path string, version int,
) (*data.Secret, error) {
return secret.Get(a.source, path, version)
}
// GetSecret retrieves the latest version of the secret at the given path.
//
// Parameters:
// - path string: Path to the secret to retrieve
//
// Returns:
// - *data.Secret: Secret data if found, nil if secret not found
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// secret, err := api.GetSecret("secret/path")
func (a *Api) GetSecret(path string) (*data.Secret, error) {
return secret.Get(a.source, path, 0)
}
// ListSecretKeys retrieves all secret keys.
//
// Returns:
// - *[]string: Pointer to array of secret keys if found, nil if none found
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// keys, err := api.ListSecretKeys()
func (a *Api) ListSecretKeys() (*[]string, error) {
return secret.ListKeys(a.source)
}
// GetSecretMetadata retrieves metadata for a specific version of a secret at
// the given path.
//
// Parameters:
// - path string: Path to the secret to retrieve metadata for
// - version int: Version number of the secret to retrieve metadata for
//
// Returns:
// - *data.SecretMetadata: Secret metadata if found, nil if secret not found
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// metadata, err := api.GetSecretMetadata("secret/path", 1)
func (a *Api) GetSecretMetadata(
path string, version int,
) (*data.SecretMetadata, error) {
return secret.GetMetadata(a.source, path, version)
}
// PutSecret creates or updates a secret at the specified path with the given
// values.
//
// Parameters:
// - path string: Path where the secret should be stored
// - data map[string]string: Map of key-value pairs representing the secret
// data
//
// Returns:
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// err := api.PutSecret("secret/path", map[string]string{"key": "value"})
func (a *Api) PutSecret(path string, data map[string]string) error {
return secret.Put(a.source, path, data)
}
// UndeleteSecret restores previously deleted versions of a secret at the
// specified path.
//
// Parameters:
// - path string: Path to the secret to restore
// - versions []int: Array of version numbers to restore. Empty array
// attempts no restoration
//
// Returns:
// - error: nil on success, unauthorized error if not logged in, or
// wrapped error on request/parsing failure
//
// Example:
//
// err := api.UndeleteSecret("secret/path", []int{1, 2})
func (a *Api) UndeleteSecret(path string, versions []int) error {
return secret.Undelete(a.source, path, versions)
}
// Recover returns recovery partitions for SPIKE Nexus to be used in a
// break-the-glass recovery operation if 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:
// - *[][32]byte: Pointer to array of recovery shards as 32-byte arrays
// - error: nil on success, unauthorized error if not authorized, or
// wrapped error on request/parsing failure
//
// Example:
//
// shards, err := api.Recover()
func (a *Api) Recover() (map[int]*[32]byte, error) {
return operator.Recover(a.source)
}
// Restore SPIKE Nexus backing using recovery shards when SPIKE Keepers cannot
// provide adequate shards and SPIKE Nexus cannot recall its root key either.
//
// This is a break-the-glass superuser-only operation that a well-architected
// SPIKE deployment should not need.
//
// Parameters:
// - shard *[32]byte: Pointer to a 32-byte array containing the shard to seed
//
// Returns:
// - *data.RestorationStatus: Status of the restoration process if successful
// - error: nil on success, unauthorized error if not authorized, or
// wrapped error on request/parsing failure
//
// Example:
//
// status, err := api.Restore(shardPtr)
func (a *Api) Restore(index int, shard *[32]byte) (*data.RestorationStatus, error) {
return operator.Restore(a.source, index, shard)
}

View File

@ -11,6 +11,8 @@ const ErrServerFault = ErrorCode("server_fault")
const ErrUnauthorized = ErrorCode("unauthorized")
const ErrInternal = ErrorCode("internal_error")
const ErrLowEntropy = ErrorCode("low_entropy")
const ErrNotReady = ErrorCode("not_ready")
const ErrNotAlive = ErrorCode("not_alive")
const ErrAlreadyInitialized = ErrorCode("already_initialized")
const ErrNotFound = ErrorCode("not_found")
const ErrSuccess = ErrorCode("success")

View File

@ -0,0 +1,29 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package data
import "time"
// SecretVersionInfo for secrets version
type SecretVersionInfo struct {
CreatedTime time.Time `json:"createdTime"`
Version int `json:"version"`
DeletedTime *time.Time `json:"deletedTime"`
}
// SecretMetaDataContent for secrets 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 secrets metadata
type SecretMetadata struct {
Versions map[int]SecretVersionInfo `json:"versions,omitempty"`
Metadata SecretMetaDataContent `json:"metadata,omitempty"`
}

View File

@ -0,0 +1,11 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package data
type RestorationStatus struct {
ShardsCollected int `json:"collected"`
ShardsRemaining int `json:"remaining"`
Restored bool `json:"restored"`
}

View File

@ -28,11 +28,11 @@ const PermissionSuper PolicyPermission = "super"
type Policy struct {
Id string `json:"id"`
Name string `json:"name"`
SpiffeIdPattern string `json:"spiffe_id_pattern"`
PathPattern string `json:"path_pattern"`
SpiffeIdPattern string `json:"spiffeIdPattern"`
PathPattern string `json:"pathPattern"`
Permissions []PolicyPermission `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
// Unexported fields won't be serialized to JSON
IdRegex *regexp.Regexp `json:"-"`

View File

@ -1,21 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// AdminTokenWriteRequest is to persist the admin token in memory.
// Admin token can be persisted only once. It is used to receive a
// short-lived session token.
type AdminTokenWriteRequest struct {
Data string `json:"data"`
}
// AdminTokenWriteResponse is to persist the admin token in memory.
type AdminTokenWriteResponse struct {
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -1,30 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
data2 "github.com/spiffe/spike-sdk-go/api/entity/data"
)
// CheckInitStateRequest is to check if the SPIKE Keep is initialized.
type CheckInitStateRequest struct {
}
// CheckInitStateResponse is to check if the SPIKE Keep is initialized.
type CheckInitStateResponse struct {
State data2.InitState `json:"state"`
Err data2.ErrorCode `json:"err,omitempty"`
}
// InitRequest is to initialize SPIKE as a superuser.
type InitRequest struct {
// Password string `json:"password"`
}
// InitResponse is to initialize SPIKE as a superuser.
type InitResponse struct {
RecoveryToken string `json:"token"`
Err data2.ErrorCode `json:"err,omitempty"`
}

View File

@ -0,0 +1,29 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import "github.com/spiffe/spike-sdk-go/api/entity/data"
// 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 data.ErrorCode `json:"err,omitempty"`
}
// RecoverRequest for disaster recovery.
type RecoverRequest struct {
}
// RecoverResponse for disaster recovery.
type RecoverResponse struct {
Shards map[int]*[32]byte `json:"shards"`
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -5,53 +5,63 @@
package reqres
import (
data2 "github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// PolicyCreateRequest for policy creation.
type PolicyCreateRequest struct {
Name string `json:"name"`
SpiffeIdPattern string `json:"spiffe_id_pattern"`
PathPattern string `json:"path_pattern"`
Permissions []data2.PolicyPermission `json:"permissions"`
Name string `json:"name"`
SpiffeIdPattern string `json:"spiffedPattern"`
PathPattern string `json:"pathPattern"`
Permissions []data.PolicyPermission `json:"permissions"`
}
// PolicyCreateResponse for policy creation.
type PolicyCreateResponse struct {
Id string `json:"id,omitempty"`
Err data2.ErrorCode `json:"err,omitempty"`
Id string `json:"id,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// PolicyReadRequest to read a policy.
type PolicyReadRequest struct {
Id string `json:"id"`
}
// PolicyReadResponse to read a policy.
type PolicyReadResponse struct {
data2.Policy
Err data2.ErrorCode `json:"err,omitempty"`
data.Policy
Err data.ErrorCode `json:"err,omitempty"`
}
// PolicyDeleteRequest to delete a policy.
type PolicyDeleteRequest struct {
Id string `json:"id"`
}
// PolicyDeleteResponse to delete a policy.
type PolicyDeleteResponse struct {
Err data2.ErrorCode `json:"err,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// PolicyListRequest to list policies.
type PolicyListRequest struct{}
// PolicyListResponse to list policies.
type PolicyListResponse struct {
Policies []data2.Policy `json:"policies"`
Err data2.ErrorCode `json:"err,omitempty"`
Policies []data.Policy `json:"policies"`
Err data.ErrorCode `json:"err,omitempty"`
}
// PolicyAccessCheckRequest to validate policy access.
type PolicyAccessCheckRequest struct {
SpiffeId string `json:"spiffe_id"`
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:"matching_policies"`
Err data2.ErrorCode `json:"err,omitempty"`
Allowed bool `json:"allowed"`
MatchingPolicies []string `json:"matchingPolicies"`
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -1,29 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// RootKeyCacheRequest is to cache the generated root key in SPIKE Keep.
// If the root key is lost due to a crash, it will be retrieved from SPIKE Keep.
type RootKeyCacheRequest struct {
RootKey string `json:"rootKey"`
}
// RootKeyCacheResponse is to cache the generated root key in SPIKE Keep.
type RootKeyCacheResponse struct {
Err data.ErrorCode `json:"error,omitempty"`
}
// RootKeyReadRequest is a request to get the root key back from remote cache.
type RootKeyReadRequest struct{}
// RootKeyReadResponse is a response to get the root key back from remote cache.
type RootKeyReadResponse struct {
RootKey string `json:"rootKey"`
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -5,17 +5,19 @@
package reqres
import (
"time"
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// SecretResponseMetadata is meta information about secrets for internal
// tracking.
type SecretResponseMetadata struct {
CreatedTime time.Time `json:"created_time"`
Version int `json:"version"`
DeletedTime *time.Time `json:"deleted_time,omitempty"`
// 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 data.ErrorCode `json:"err,omitempty"`
}
// SecretPutRequest for creating/updating secrets
@ -27,7 +29,6 @@ type SecretPutRequest struct {
// SecretPutResponse is after successful secret write
type SecretPutResponse struct {
SecretResponseMetadata
Err data.ErrorCode `json:"err,omitempty"`
}
@ -40,8 +41,7 @@ type SecretReadRequest struct {
// SecretReadResponse is for getting secrets
type SecretReadResponse struct {
data.Secret
Data map[string]string `json:"data"`
Err data.ErrorCode `json:"err,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// SecretDeleteRequest for soft-deleting secret versions
@ -52,8 +52,7 @@ type SecretDeleteRequest struct {
// SecretDeleteResponse after soft-delete
type SecretDeleteResponse struct {
Metadata SecretResponseMetadata `json:"metadata"`
Err data.ErrorCode `json:"err,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// SecretUndeleteRequest for recovering soft-deleted versions
@ -64,8 +63,7 @@ type SecretUndeleteRequest struct {
// SecretUndeleteResponse after recovery
type SecretUndeleteResponse struct {
Metadata SecretResponseMetadata `json:"metadata"`
Err data.ErrorCode `json:"err,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// SecretListRequest for listing secrets
@ -77,32 +75,3 @@ type SecretListResponse struct {
Keys []string `json:"keys"`
Err data.ErrorCode `json:"err,omitempty"`
}
// SecretMetadataReadRequest for get secrets metadata
type SecretMetadataReadRequest struct {
Path string `json:"path"`
Version int `json:"version,omitempty"` // Optional specific version
}
// SecretMetadataResponse for secrets versions and metadata
type SecretMetadataResponse struct {
Versions map[int]SecretMetadataVersionResponse `json:"versions,omitempty"`
Metadata SecretRawMetadataResponse `json:"metadata,omitempty"`
Err data.ErrorCode `json:"err,omitempty"`
}
// SecretMetadataVersionResponse for secrets version
type SecretMetadataVersionResponse struct {
CreatedTime time.Time `json:"createdTime"`
Version int `json:"version"`
DeletedTime *time.Time `json:"deletedTime"`
}
// SecretRawMetadataResponse for secrets raw metadata
type SecretRawMetadataResponse struct {
CurrentVersion int `json:"currentVersion"`
OldestVersion int `json:"oldestVersion"`
CreatedTime time.Time `json:"createdTime"`
UpdatedTime time.Time `json:"updatedTime"`
MaxVersions int `json:"maxVersions"`
}

View File

@ -0,0 +1,28 @@
package reqres
import "github.com/spiffe/spike-sdk-go/api/entity/data"
// ShardContributionRequest 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 ShardContributionRequest struct {
Shard *[32]byte `json:"shard"`
}
// ShardContributionResponse represents the response structure for a shard
// contribution.
type ShardContributionResponse struct {
Err data.ErrorCode `json:"err,omitempty"`
}
// ShardRequest represents a request to handle data partitioning or sharding.
type ShardRequest struct {
}
// ShardResponse represents the result of an operation on a specific data shard.
// The struct includes the shard identifier and an associated error code.
type ShardResponse struct {
Shard *[32]byte `json:"shard"`
Err data.ErrorCode
}

View File

@ -13,3 +13,7 @@ var ErrReadFailure = errors.New("failed to read request body")
var ErrMarshalFailure = errors.New("failed to marshal response body")
var ErrAlreadyInitialized = errors.New("already initialized")
var ErrMissingRootKey = errors.New("missing root key")
var ErrInvalidInput = errors.New("invalid input")
var ErrInvalidPermission = errors.New("invalid permission")
var ErrPeerConnection = errors.New("problem connecting to peer")
var ErrReadingResponseBody = errors.New("problem reading response body")

View File

@ -30,34 +30,3 @@ func SpikeNexusDataFolder() string {
// The data dir is not configurable for security reasons.
return filepath.Join(spikeDir, "/data")
}
// SpikePilotRecoveryFolder returns the path to the directory where Pilot stores
// recovery material for its root key.
func SpikePilotRecoveryFolder() string {
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir = "/tmp"
}
spikeDir := filepath.Join(homeDir, ".spike")
// Create 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(spikeDir+"/recovery", 0700)
if err != nil {
panic(err)
}
// The data dir is not configurable for security reasons.
return filepath.Join(spikeDir, "/recovery")
}
// SpikePilotRootKeyRecoveryFile returns the path to the file where Pilot stores
// the root key recovery file.
func SpikePilotRootKeyRecoveryFile() string {
folder := SpikePilotRecoveryFolder()
// The file path and file name are NOT configurable for security reasons.
return filepath.Join(folder, ".root-key-recovery.spike")
}

View File

@ -0,0 +1,109 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"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/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 using
// the X.509 source and sends a policy creation request to the server.
//
// The function takes the following parameters:
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
// connection
// - 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 for
// this policy
//
// The function returns an error if any of the following operations fail:
// - Marshaling the policy creation request
// - Creating the mTLS client
// - Making the HTTP POST request
// - Unmarshaling the response
// - Server-side policy creation (indicated in the response)
//
// Example usage:
//
// source, err := workloadapi.NewX509Source(context.Background())
// 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)
// return
// }
func CreatePolicy(source *workloadapi.X509Source,
name string, spiffeIdPattern string, pathPattern string,
permissions []data.PolicyPermission,
) error {
r := reqres.PolicyCreateRequest{
Name: name,
SpiffeIdPattern: spiffeIdPattern,
PathPattern: pathPattern,
Permissions: permissions,
}
mr, err := json.Marshal(r)
if err != nil {
return errors.Join(
errors.New(
"createPolicy: I am having problem generating the payload",
),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return err
}
body, err := net.Post(client, url.PolicyCreate(), mr)
if err != nil {
return err
}
res := reqres.PolicyCreateResponse{}
err = json.Unmarshal(body, &res)
if err != nil {
return errors.Join(
errors.New("createPolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return errors.New(string(res.Err))
}
return nil
}

View File

@ -0,0 +1,85 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/net"
)
// DeletePolicy removes an existing policy from the system using its ID.
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
// to make the deletion request.
//
// The function takes the following parameters:
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
// connection
// - id: The unique identifier of the policy to be deleted
//
// The function returns an error if any of the following operations fail:
// - Marshaling the policy deletion request
// - Creating the mTLS client
// - Making the HTTP POST request
// - Unmarshaling the response
// - Server-side policy deletion (indicated in the response)
//
// Example usage:
//
// source, err := workloadapi.NewX509Source(context.Background())
// 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)
// return
// }
func DeletePolicy(source *workloadapi.X509Source, id string) error {
r := reqres.PolicyDeleteRequest{
Id: id,
}
mr, err := json.Marshal(r)
if err != nil {
return errors.Join(
errors.New(
"deletePolicy: I am having problem generating the payload",
),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return err
}
body, err := net.Post(client, url.PolicyDelete(), mr)
if err != nil {
return err
}
res := reqres.PolicyDeleteResponse{}
err = json.Unmarshal(body, &res)
if err != nil {
return errors.Join(
errors.New("deletePolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return errors.New(string(res.Err))
}
return nil
}

View File

@ -0,0 +1,98 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"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/net"
)
// GetPolicy retrieves a policy from the system using its ID.
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
// to make the retrieval request.
//
// The function takes the following parameters:
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
// connection
// - id: The unique identifier of the policy to retrieve
//
// The function returns:
// - (*data.Policy, nil) if the policy is found
// - (nil, nil) if the policy is not found
// - (nil, error) if an error occurs during the operation
//
// Errors can occur during:
// - Marshaling the policy retrieval request
// - Creating the mTLS client
// - Making the HTTP POST request (except for not found cases)
// - Unmarshaling the response
// - Server-side policy retrieval (indicated in the response)
//
// Example usage:
//
// source, err := workloadapi.NewX509Source(context.Background())
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// policy, err := GetPolicy(source, "policy-123")
// if err != nil {
// log.Printf("Error retrieving policy: %v", err)
// return
// }
// if policy == nil {
// log.Printf("Policy not found")
// return
// }
//
// log.Printf("Found policy: %+v", policy)
func GetPolicy(
source *workloadapi.X509Source, id string,
) (*data.Policy, error) {
r := reqres.PolicyReadRequest{Id: id}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("getPolicy: I am having problem generating the payload"),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.PolicyGet(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.PolicyReadResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("getPolicy: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &res.Policy, nil
}

View File

@ -0,0 +1,103 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"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/net"
)
// ListPolicies retrieves all policies from the system.
// It requires a SPIFFE X.509 source for establishing a mutual TLS connection
// to make the list request.
//
// The function takes:
// - source: A pointer to a workloadapi.X509Source for establishing mTLS
// connection
//
// The function returns:
// - (*[]data.Policy, nil) containing all policies if successful
// - (nil, nil) if no policies are found
// - (nil, error) if an error occurs during the operation
//
// Note: The returned slice pointer should be dereferenced before use:
//
// policies := *result
//
// Errors can occur during:
// - Marshaling the policy list request
// - Creating the mTLS client
// - Making the HTTP POST request (except for not found cases)
// - Unmarshaling the response
// - Server-side policy listing (indicated in the response)
//
// Example usage:
//
// source, err := workloadapi.NewX509Source(context.Background())
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// 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) (*[]data.Policy, error) {
r := reqres.PolicyListRequest{}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New(
"listPolicies: I am having problem generating the payload",
),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.PolicyList(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.PolicyListResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("listPolicies: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &res.Policies, nil
}

View File

@ -0,0 +1,80 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"errors"
"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/net"
)
// Recover makes a request to initiate recovery of secrets, returning the
// recovery shards.
//
// Parameters:
// - source: X509Source used for mTLS client authentication
//
// Returns:
// - map[int]*[32]byte: Map of shard indices to shard byte arrays if
// successful, nil if not found
// - error: nil on success, error if:
// - Failed to marshal recover request
// - Failed to create mTLS client
// - Request failed (except for not found case)
// - Failed to parse response body
// - Server returned error in response
//
// Example:
//
// shards, err := Recover(x509Source)
func Recover(source *workloadapi.X509Source) (map[int]*[32]byte, error) {
r := reqres.RecoverRequest{}
mr, err := json.Marshal(r)
if err != nil {
return nil, errors.Join(
errors.New("recover: failed to marshal recover request"),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
body, err := net.Post(client, url.Recover(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.RecoverResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("recover: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
result := make(map[int]*[32]byte)
for i, shard := range res.Shards {
result[i] = shard
}
return result, nil
}

View File

@ -0,0 +1,101 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"errors"
"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"
"github.com/spiffe/spike-sdk-go/net"
)
// Restore submits a recovery shard to continue the restoration process.
//
// 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, nil if not found
// - error: nil on success, error if:
// - Failed to marshal restore request
// - Failed to create mTLS client
// - Request failed (except for not found case)
// - Failed to parse response body
// - Server returned error in response
//
// Example:
//
// status, err := Restore(x509Source, shardIndex, shardValue)
func Restore(
source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte,
) (*data.RestorationStatus, error) {
r := reqres.RestoreRequest{
Id: shardIndex,
Shard: shardValue,
}
mr, err := 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 err != nil {
return nil, errors.Join(
errors.New("restore: failed to marshal recover request"),
err,
)
}
client, err := net.CreateMtlsClient(source)
if err != nil {
// Security: Zero out mr before returning error
for i := range mr {
mr[i] = 0
}
return nil, err
}
body, err := net.Post(client, url.Restore(), mr)
// Security: Zero out mr after post request is complete
for i := range mr {
mr[i] = 0
}
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return nil, nil
}
return nil, err
}
var res reqres.RestoreResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, errors.Join(
errors.New("recover: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return nil, errors.New(string(res.Err))
}
return &data.RestorationStatus{
ShardsCollected: res.ShardsCollected,
ShardsRemaining: res.ShardsRemaining,
Restored: res.Restored,
}, nil
}

View File

@ -1,18 +1,21 @@
package api
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"errors"
"strconv"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// DeleteSecret deletes specified versions of a secret at the given path using
// Delete deletes specified versions of a secret at the given path using
// mTLS authentication.
//
// It converts string version numbers to integers, constructs a delete request,
@ -30,25 +33,12 @@ import (
//
// Example:
//
// err := DeleteSecret(x509Source, "secret/path", []string{"1", "2"})
func DeleteSecret(source *workloadapi.X509Source,
path string, versions []string) error {
var vv []int
if len(versions) == 0 {
vv = []int{}
}
for _, version := range versions {
v, e := strconv.Atoi(version)
if e != nil {
continue
}
vv = append(vv, v)
}
// err := deleteSecret(x509Source, "secret/path", []string{"1", "2"})
func Delete(source *workloadapi.X509Source,
path string, versions []int) error {
r := reqres.SecretDeleteRequest{
Path: path,
Versions: vv,
Versions: versions,
}
mr, err := json.Marshal(r)
@ -61,8 +51,7 @@ func DeleteSecret(source *workloadapi.X509Source,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return err
}

View File

@ -2,22 +2,22 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"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"
code "github.com/spiffe/spike-sdk-go/api/errors"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// GetSecret retrieves a specific version of a secret at the given path using
// Get retrieves a specific version of a secret at the given path using
// mTLS authentication.
//
// Parameters:
@ -32,8 +32,8 @@ import (
//
// Example:
//
// secret, err := GetSecret(x509Source, "secret/path", 1)
func GetSecret(source *workloadapi.X509Source,
// secret, err := getSecret(x509Source, "secret/path", 1)
func Get(source *workloadapi.X509Source,
path string, version int) (*data.Secret, error) {
r := reqres.SecretReadRequest{
Path: path,
@ -48,8 +48,7 @@ func GetSecret(source *workloadapi.X509Source,
)
}
truer := func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}

View File

@ -2,20 +2,20 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// ListSecretKeys retrieves all secret keys using mTLS authentication.
// ListKeys retrieves all secret keys using mTLS authentication.
//
// Parameters:
// - source: X509Source for mTLS client authentication
@ -27,12 +27,12 @@ import (
//
// Example:
//
// keys, err := ListSecretKeys(x509Source)
func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) {
// keys, err := listSecretKeys(x509Source)
func ListKeys(source *workloadapi.X509Source) (*[]string, error) {
r := reqres.SecretListRequest{}
mr, err := json.Marshal(r)
if err != nil {
return []string{}, errors.Join(
return nil, errors.Join(
errors.New(
"listSecretKeys: I am having problem generating the payload",
),
@ -40,31 +40,30 @@ func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) {
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return []string{}, err
return nil, err
}
body, err := net.Post(client, url.SecretList(), mr)
if err != nil {
if errors.Is(err, net.ErrNotFound) {
return []string{}, nil
return &[]string{}, nil
}
return []string{}, err
return nil, err
}
var res reqres.SecretListResponse
err = json.Unmarshal(body, &res)
if err != nil {
return []string{}, errors.Join(
return nil, errors.Join(
errors.New("getSecret: Problem parsing response body"),
err,
)
}
if res.Err != "" {
return []string{}, errors.New(string(res.Err))
return nil, errors.New(string(res.Err))
}
return res.Keys, nil
return &res.Keys, nil
}

View File

@ -2,21 +2,22 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"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/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// GetSecretMetadata retrieves a specific version of a secret metadata at the given path using
// mTLS authentication.
// GetMetadata retrieves a specific version of a secret metadata at the
// given path using mTLS authentication.
//
// Parameters:
// - source: X509Source for mTLS client authentication
@ -30,11 +31,11 @@ import (
//
// Example:
//
// metadata, err := GetSecretMetadata(x509Source, "secret/path", 1)
func GetSecretMetadata(
// metadata, err := getSecretMetadata(x509Source, "secret/path", 1)
func GetMetadata(
source *workloadapi.X509Source, path string, version int,
) (*reqres.SecretMetadataResponse, error) {
r := reqres.SecretReadRequest{
) (*data.SecretMetadata, error) {
r := reqres.SecretMetadataRequest{
Path: path,
Version: version,
}
@ -47,8 +48,7 @@ func GetSecretMetadata(
)
}
truer := func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return nil, err
}
@ -73,5 +73,8 @@ func GetSecretMetadata(
return nil, errors.New(string(res.Err))
}
return &res, nil
return &data.SecretMetadata{
Versions: res.Versions,
Metadata: res.Metadata,
}, nil
}

View File

@ -2,20 +2,20 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
"errors"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// PutSecret creates or updates a secret at the specified path with the given
// Put creates or updates a secret at the specified path with the given
// values using mTLS authentication.
//
// Parameters:
@ -29,8 +29,9 @@ import (
//
// Example:
//
// err := PutSecret(x509Source, "secret/path", map[string]string{"key": "value"})
func PutSecret(source *workloadapi.X509Source,
// err := putSecret(x509Source, "secret/path",
// map[string]string{"key": "value"})
func Put(source *workloadapi.X509Source,
path string, values map[string]string) error {
r := reqres.SecretPutRequest{
@ -46,8 +47,7 @@ func PutSecret(source *workloadapi.X509Source,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return err
}

View File

@ -2,21 +2,20 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
package secret
import (
"encoding/json"
"errors"
"strconv"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// UndeleteSecret restores previously deleted versions of a secret at the
// Undelete restores previously deleted versions of a secret at the
// specified path using mTLS authentication.
//
// Parameters:
@ -31,22 +30,14 @@ import (
//
// Example:
//
// err := UndeleteSecret(x509Source, "secret/path", []string{"1", "2"})
func UndeleteSecret(source *workloadapi.X509Source,
path string, versions []string) error {
// err := undeleteSecret(x509Source, "secret/path", []string{"1", "2"})
func Undelete(source *workloadapi.X509Source,
path string, versions []int) error {
var vv []int
if len(versions) == 0 {
vv = []int{}
}
for _, version := range versions {
v, e := strconv.Atoi(version)
if e != nil {
continue
}
vv = append(vv, v)
}
r := reqres.SecretUndeleteRequest{
Path: path,
Versions: vv,
@ -62,8 +53,7 @@ func UndeleteSecret(source *workloadapi.X509Source,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
client, err := net.CreateMtlsClient(source)
if err != nil {
return err
}

View File

@ -1,31 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
type SpikeNexusApiAction string
const keyApiAction = "action"
const actionNexusCheck SpikeNexusApiAction = "check"
const actionNexusGet SpikeNexusApiAction = "get"
const actionNexusDelete SpikeNexusApiAction = "delete"
const actionNexusUndelete SpikeNexusApiAction = "undelete"
const actionNexusList SpikeNexusApiAction = "list"
const actionNexusDefault SpikeNexusApiAction = ""
type SpikeKeeperApiAction string
const actionKeeperRead SpikeKeeperApiAction = "read"
const actionKeeperDefault SpikeKeeperApiAction = ""
type ApiUrl string
const spikeNexusUrlSecrets ApiUrl = "/v1/store/secrets"
const spikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata"
const spikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
const spikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
const spikeKeeperUrlKeep ApiUrl = "/v1/store/keep"

View File

@ -1,104 +0,0 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"encoding/json"
"errors"
"fmt"
"os"
"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/internal/config"
"github.com/spiffe/spike-sdk-go/api/internal/url"
"github.com/spiffe/spike-sdk-go/net"
)
// Init sends an initialization request to SPIKE Nexus.
func Init(source *workloadapi.X509Source) error {
r := reqres.InitRequest{}
mr, err := json.Marshal(r)
if err != nil {
return errors.Join(
errors.New(
"initialization: I am having problem generating the payload",
),
err,
)
}
// TODO: maybe restrict init and ACL operations at this level without
// doing any policy checks.
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
body, err := net.Post(client, url.Init(), mr)
var res reqres.InitResponse
err = json.Unmarshal(body, &res)
if err != nil {
return errors.Join(
errors.New("initialization: Problem parsing response body"),
err,
)
}
if res.RecoveryToken == "" {
fmt.Println("Failed to get recovery token")
return errors.New("failed to get recovery token")
}
err = os.WriteFile(
config.SpikePilotRootKeyRecoveryFile(), []byte(res.RecoveryToken), 0600,
)
if err != nil {
fmt.Println("Failed to save token to file:")
fmt.Println(err.Error())
return errors.New("failed to save token to file")
}
return nil
}
// CheckInitState sends a checkInitState request to SPIKE Nexus.
func CheckInitState(source *workloadapi.X509Source) (data.InitState, error) {
r := reqres.CheckInitStateRequest{}
mr, err := json.Marshal(r)
if err != nil {
return data.NotInitialized, errors.Join(
errors.New(
"checkInitState: I am having problem generating the payload",
),
err,
)
}
var truer = func(string) bool { return true }
client, err := net.CreateMtlsClient(source, truer)
body, err := net.Post(client, url.InitState(), mr)
if err != nil {
return data.NotInitialized, errors.Join(
errors.New(
"checkInitState: I am having problem sending the request",
), err)
}
var res reqres.CheckInitStateResponse
err = json.Unmarshal(body, &res)
if err != nil {
return data.NotInitialized, errors.Join(
errors.New("checkInitState: Problem parsing response body"),
err,
)
}
state := res.State
return state, nil
}

32
api/url/config.go Normal file
View File

@ -0,0 +1,32 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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 SpikeNexusUrlSecrets ApiUrl = "/v1/store/secrets"
const SpikeNexusUrlSecretsMetadata ApiUrl = "/v1/store/secrets/metadata"
const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
const SpikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
const SpikeNexusUrlOperatorRecover ApiUrl = "/v1/operator/recover"
const SpikeNexusUrlOperatorRestore ApiUrl = "/v1/operator/restore"
const SpikeKeeperUrlKeep ApiUrl = "/v1/store/keep"
const SpikeKeeperUrlContribute ApiUrl = "/v1/store/contribute"
const SpikeKeeperUrlShard ApiUrl = "/v1/store/shard"

27
api/url/operator.go Normal file
View File

@ -0,0 +1,27 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
func Restore() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(SpikeNexusUrlOperatorRestore),
)
return u
}
func Recover() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(SpikeNexusUrlOperatorRecover),
)
return u
}

View File

@ -10,44 +10,44 @@ import (
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
// UrlPolicyCreate returns the URL for creating a policy.
// PolicyCreate returns the URL for creating a policy.
func PolicyCreate() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlPolicy),
string(SpikeNexusUrlPolicy),
)
return u
}
// UrlPolicyList returns the URL for listing policies.
// PolicyList returns the URL for listing policies.
func PolicyList() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlPolicy),
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusList))
params.Add(KeyApiAction, string(ActionList))
return u + "?" + params.Encode()
}
// UrlPolicyDelete returns the URL for deleting a policy.
// PolicyDelete returns the URL for deleting a policy.
func PolicyDelete() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlPolicy),
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusDelete))
params.Add(KeyApiAction, string(ActionDelete))
return u + "?" + params.Encode()
}
// UrlPolicyGet returns the URL for getting a policy.
// PolicyGet returns the URL for getting a policy.
func PolicyGet() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlPolicy),
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusGet))
params.Add(KeyApiAction, string(ActionGet))
return u + "?" + params.Encode()
}

View File

@ -10,66 +10,66 @@ import (
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
// UrlSecretGet returns the URL for getting a secret.
// SecretGet returns the URL for getting a secret.
func SecretGet() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecrets),
string(SpikeNexusUrlSecrets),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusGet))
params.Add(KeyApiAction, string(ActionGet))
return u + "?" + params.Encode()
}
// UrlSecretPut returns the URL for putting a secret.
// SecretPut returns the URL for putting a secret.
func SecretPut() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecrets),
string(SpikeNexusUrlSecrets),
)
return u
}
// UrlSecretDelete returns the URL for deleting a secret.
// SecretDelete returns the URL for deleting a secret.
func SecretDelete() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecrets),
string(SpikeNexusUrlSecrets),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusDelete))
params.Add(KeyApiAction, string(ActionDelete))
return u + "?" + params.Encode()
}
// UrlSecretUndelete returns the URL for undeleting a secret.
// SecretUndelete returns the URL for undeleting a secret.
func SecretUndelete() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecrets),
string(SpikeNexusUrlSecrets),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusUndelete))
params.Add(KeyApiAction, string(ActionUndelete))
return u + "?" + params.Encode()
}
// UrlSecretList returns the URL for listing secrets.
// SecretList returns the URL for listing secrets.
func SecretList() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecrets),
string(SpikeNexusUrlSecrets),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusList))
params.Add(KeyApiAction, string(ActionList))
return u + "?" + params.Encode()
}
// UrlSecretMetadataGet returns the URL for getting a secret metadata.
// SecretMetadataGet returns the URL for getting a secret metadata.
func SecretMetadataGet() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlSecretsMetadata),
string(SpikeNexusUrlSecretsMetadata),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusGet))
params.Add(KeyApiAction, string(ActionGet))
return u + "?" + params.Encode()
}

View File

@ -10,23 +10,23 @@ import (
"github.com/spiffe/spike-sdk-go/api/internal/env"
)
// UrlInit returns the URL for initializing SPIKE Nexus.
// Init returns the URL for initializing SPIKE Nexus.
func Init() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlInit),
string(SpikeNexusUrlInit),
)
return u
}
// UrlInitState returns the URL for checking the initialization state of
// InitState returns the URL for checking the initialization state of
// SPIKE Nexus.
func InitState() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(spikeNexusUrlInit),
string(SpikeNexusUrlInit),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusCheck))
params.Add(KeyApiAction, string(ActionCheck))
return u + "?" + params.Encode()
}

42
crypto/algo.go Normal file
View File

@ -0,0 +1,42 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
)
var reader = rand.Read
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.
// - error: Returns nil on successful key generation, or an error if the random
// number 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, error) {
// Generate a 256-bit key
key := make([]byte, aes256KeySize)
_, err := reader(key)
if err != nil {
return "", errors.Join(
err,
errors.New("Aes256Seed: failed to generate random key"),
)
}
return hex.EncodeToString(key), nil
}

43
crypto/id.go Normal file
View File

@ -0,0 +1,43 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package crypto
import "fmt"
const letters = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// RandomString generates a cryptographically-unique secure random string.
func RandomString(n int) (string, error) {
bytes := make([]byte, n)
if _, err := reader(bytes); err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
// Token generates a cryptographically-unique secure random string.
func Token() string {
id, err := RandomString(26)
if err != nil {
id = fmt.Sprintf("CRYPTO-ERR: %s", err.Error())
}
return "spike." + id
}
// Id generates a cryptographically-unique secure random string.
func Id() string {
id, err := RandomString(8)
if err != nil {
id = fmt.Sprintf("CRYPTO-ERR: %s", err.Error())
}
return id
}

56
crypto/io.go Normal file
View File

@ -0,0 +1,56 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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.
//
// Parameters:
// - p []byte: Buffer to read data into
//
// Returns:
// - n int: Number of bytes read
// - err error: Always nil as 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,
}
}

32
go.mod
View File

@ -1,18 +1,28 @@
module github.com/spiffe/spike-sdk-go
go 1.23.3
go 1.24
require github.com/spiffe/go-spiffe/v2 v2.4.0
toolchain go1.24.1
require (
github.com/cenkalti/backoff/v4 v4.3.0
github.com/google/uuid v1.6.0
github.com/spiffe/go-spiffe/v2 v2.5.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/zeebo/errs v1.3.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/grpc v1.72.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

163
go.sum
View File

@ -1,32 +1,171 @@
cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y=
cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
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.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c=
github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/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=

10
hack/tag.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# \\ SPIKE: Secure your secrets with SPIFFE.
# \\\\\ Copyright 2024-present SPIKE contributors.
# \\\\\\\ SPDX-License-Identifier: Apache-2.0
VERSION="v0.5.13"
git tag -s "$VERSION" -m "$VERSION"
git push origin --tags

54
kv/delete.go Normal file
View File

@ -0,0 +1,54 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import (
"time"
)
// Delete marks secret versions as deleted for a given path. If no versions are
// specified, it marks only the current version as deleted. If specific versions
// are provided, it marks each existing version in the list as deleted. The
// deletion is performed by setting the DeletedTime to the current time. If the
// path doesn't exist, the function returns without making any changes.
func (kv *KV) Delete(path string, versions []int) error {
secret, exists := kv.data[path]
if !exists {
return ErrItemNotFound
}
now := time.Now()
cv := secret.Metadata.CurrentVersion
// If no versions specified, mark the latest version as deleted
if len(versions) == 0 {
if v, exists := secret.Versions[cv]; exists {
v.DeletedTime = &now // Mark as deleted.
secret.Versions[cv] = v
}
return nil
}
// Delete specific versions
for _, version := range versions {
if version == 0 {
v, exists := secret.Versions[cv]
if !exists {
continue
}
v.DeletedTime = &now // Mark as deleted.
secret.Versions[cv] = v
continue
}
if v, exists := secret.Versions[version]; exists {
v.DeletedTime = &now // Mark as deleted.
secret.Versions[version] = v
}
}
return nil
}

120
kv/delete_test.go Normal file
View File

@ -0,0 +1,120 @@
package kv
import (
"testing"
)
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: ErrItemNotFound,
},
{
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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kv := tt.setup()
err := kv.Delete(tt.path, tt.versions)
if err != tt.wantErr {
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr == 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)
}
}
}
}
}
})
}
}

9
kv/doc.go Normal file
View File

@ -0,0 +1,9 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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

56
kv/entity.go Normal file
View File

@ -0,0 +1,56 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import "time"
// Version represents a single version of a secret's 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 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 a secret and its versions.
// It maintains version boundaries and timestamps for the overall secret.
type Metadata struct {
// CurrentVersion is the newest/latest version number of the secret
CurrentVersion int
// OldestVersion is the oldest available version number of the secret
OldestVersion int
// CreatedTime is when the secret was first created
CreatedTime time.Time
// UpdatedTime is when the secret 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 secret
Metadata Metadata
}

14
kv/err.go Normal file
View File

@ -0,0 +1,14 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import "errors"
var (
ErrVersionNotFound = errors.New("version not found")
ErrItemNotFound = errors.New("item not found")
ErrItemSoftDeleted = errors.New("item marked as deleted")
ErrInvalidVersion = errors.New("invalid version")
)

80
kv/get.go Normal file
View File

@ -0,0 +1,80 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// 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 and false if the path doesn't exist
// - Returns nil and false if the specified version doesn't exist
// - Returns nil and false 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
// - bool: true if data was found and is valid, false otherwise
//
// Example usage:
//
// kv := &KV{}
// // Get current version
// data, exists := kv.Get("secret/myapp", 0)
//
// // Get specific version
// historicalData, exists := kv.Get("secret/myapp", 2)
func (kv *KV) Get(path string, version int) (map[string]string, error) {
secret, exists := kv.data[path]
if !exists {
return nil, ErrItemNotFound
}
// #region debug
// fmt.Println("########")
// vv := secret.Versions
// for i, v := range vv {
// fmt.Println("version", i, "version:", v.Version, "created:",
// v.CreatedTime, "deleted:", v.DeletedTime, "data:", v.Data)
// }
// fmt.Println("########")
// #endregion
// If version not specified, use current version
if version == 0 {
version = secret.Metadata.CurrentVersion
}
v, exists := secret.Versions[version]
if !exists || v.DeletedTime != nil {
return nil, ErrItemSoftDeleted
}
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.
//
// Parameters:
// - path: The path to retrieve the secret from
//
// Returns:
// - *Value: The secret at the specified path, or nil if it doesn't exist
// or has been deleted.
func (kv *KV) GetRawSecret(path string) (*Value, error) {
secret, exists := kv.data[path]
if !exists {
return nil, ErrItemNotFound
}
return secret, nil
}

281
kv/get_test.go Normal file
View File

@ -0,0 +1,281 @@
package kv
import (
"testing"
"time"
)
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: ErrItemNotFound,
},
{
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: ErrItemSoftDeleted,
},
{
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: ErrItemSoftDeleted,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kv := tt.setup()
got, err := kv.Get(tt.path, tt.version)
if err != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr == 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: ErrItemNotFound,
},
{
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)
if err != tt.wantErr {
t.Errorf("GetRawSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr == 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])
}
}
}
}
})
}
}

64
kv/import.go Normal file
View File

@ -0,0 +1,64 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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.
//
// If a secret already exists in the store, it will be overwritten with the
// imported value. The method preserves all version history and metadata from
// the imported secrets.
//
// Example usage:
//
// secrets, err := persistentStore.LoadAllSecrets(context.Background())
// if err != nil {
// log.Fatalf("Failed to load secrets: %v", err)
// }
// kvStore.ImportSecrets(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
}
}

24
kv/kv.go Normal file
View File

@ -0,0 +1,24 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// KV represents an in-memory key-value store with versioning
type KV struct {
maxSecretVersions int
data map[string]*Value
}
// Config represents the configuration for a KV instance
type Config struct {
MaxSecretVersions int
}
// New creates a new KV instance
func New(config Config) *KV {
return &KV{
maxSecretVersions: config.MaxSecretVersions,
data: make(map[string]*Value),
}
}

21
kv/list.go Normal file
View File

@ -0,0 +1,21 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// List returns a slice containing all keys stored in the key-value store.
// The order of keys in the returned slice is not guaranteed to be stable
// between calls.
//
// Returns:
// - []string: A slice containing all keys present in the store
func (kv *KV) List() []string {
keys := make([]string, 0, len(kv.data))
for k := range kv.data {
keys = append(keys, k)
}
return keys
}

61
kv/list_test.go Normal file
View File

@ -0,0 +1,61 @@
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)
}
}
})
}
}

85
kv/put.go Normal file
View File

@ -0,0 +1,85 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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 with a maximum of 3 versions per
// path.
//
// When storing values:
// - If the path doesn't exist, it creates a new secret with initial metadata
// - Each put operation creates a new version with an incremented version
// number
// - Old versions are automatically pruned when exceeding MaxVersions
// (default: 10)
// - Timestamps are updated for both creation and modification times
//
// Parameters:
// - path: The location where the secret will be stored
// - values: A map of key-value pairs to store at this path
//
// The function maintains metadata including:
// - CreatedTime: When the secret was first created
// - UpdatedTime: When the most recent version was added
// - CurrentVersion: The latest version number
// - OldestVersion: The oldest available version number
// - MaxVersions: Maximum number of versions to keep (fixed at 10)
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 version will
// default to the current version.
CurrentVersion: 1,
OldestVersion: 1,
},
}
kv.data[path] = secret
} else {
secret.Metadata.CurrentVersion++
}
newVersion := secret.Metadata.CurrentVersion
// Add new version
secret.Versions[newVersion] = Version{
Data: values,
CreatedTime: rightNow,
Version: newVersion,
}
// Update metadata
secret.Metadata.UpdatedTime = rightNow
// Cleanup 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
}
}

94
kv/put_test.go Normal file
View File

@ -0,0 +1,94 @@
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)
}
})
}
}

57
kv/undelete.go Normal file
View File

@ -0,0 +1,57 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// Undelete restores previously deleted versions of a secret at the specified
// path. It sets the DeletedTime to nil for each specified version that exists.
//
// Parameters:
// - path: The location of the secret in the store
// - versions: A slice of version numbers to undelete
//
// Returns:
// - error: ErrItemNotFound if the path doesn't exist, nil on success
//
// If a version number in the versions slice doesn't exist, it is silently
// skipped without returning an error. Only existing versions are modified.
func (kv *KV) Undelete(path string, versions []int) error {
secret, exists := kv.data[path]
if !exists {
return ErrItemNotFound
}
cv := secret.Metadata.CurrentVersion
// If no versions specified, mark the latest version as undeleted
if len(versions) == 0 {
if v, exists := secret.Versions[cv]; exists {
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[cv] = v
}
return nil
}
// Delete specific versions
for _, version := range versions {
if version == 0 {
v, exists := secret.Versions[cv]
if !exists {
continue
}
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[cv] = v
continue
}
if v, exists := secret.Versions[version]; exists {
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[version] = v
}
}
return nil
}

182
kv/undelete_test.go Normal file
View File

@ -0,0 +1,182 @@
package kv
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
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 spesific 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: ErrItemNotFound,
},
{
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)
assert.Equal(t, tt.wantErr, err)
if err == nil {
secret, exist := kv.data[tt.path]
assert.True(t, exist)
for _, version := range tt.versions {
if version == 0 {
version = secret.Metadata.CurrentVersion
}
if v, exist := secret.Versions[version]; exist {
assert.True(t, exist)
assert.Nil(t, v.DeletedTime)
}
}
}
})
}
}

View File

@ -7,13 +7,32 @@ package net
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
func RequestBody(r *http.Request) (bod []byte, err error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
defer func(b io.ReadCloser) {
if b == nil {
return
}
err = errors.Join(err, b.Close())
}(r.Body)
return body, err
}
// CreateMtlsServer 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
@ -56,8 +75,28 @@ func CreateMtlsServer(source *workloadapi.X509Source,
return server, nil
}
// TODO: document.
func CreateMtlsClient(
// CreateMtlsClientWithPredicate creates an HTTP client configured for mutual TLS
// authentication using SPIFFE workload identities.
// It uses the provided X.509 source for client certificates and validates peer
// certificates against a predicate function.
//
// Parameters:
// - source: An X509Source that provides the client's identity certificates
// and trusted roots
// - predicate: A function that evaluates SPIFFE IDs (as strings) and returns
// true if the ID should be trusted
//
// Returns:
// - *http.Client: A configured HTTP client that will use mTLS for all
// connections
// - error: An error if the client creation fails
//
// The returned client will:
// - Present client certificates from the provided X509Source
// - Validate peer certificates using the same X509Source
// - Only accept peer certificates with SPIFFE IDs that pass the predicate
// function
func CreateMtlsClientWithPredicate(
source *workloadapi.X509Source,
predicate func(string) bool,
) (*http.Client, error) {
@ -75,22 +114,58 @@ func CreateMtlsClient(
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
IdleConnTimeout: 30 * time.Second,
MaxIdleConns: 100,
MaxConnsPerHost: 10,
MaxIdleConnsPerHost: 10,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 5 * time.Second,
},
Timeout: 60 * time.Second,
}
return client, 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.
// CreateMtlsClient creates an HTTP client configured for mutual TLS
// authentication using SPIFFE workload identities.
// It uses the provided X.509 source for client certificates and validates peer
// certificates against a predicate function.
//
// 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
// - error: An error if the client creation fails
//
// The returned client will:
// - Present client certificates from the provided X509Source
// - Validate peer certificates using the same X509Source
// - Only accept peer certificates with SPIFFE IDs that pass the predicate
// function
func CreateMtlsClient(source *workloadapi.X509Source) (*http.Client, error) {
return CreateMtlsClientWithPredicate(source, func(string) bool { return true })
}
// 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 on
// (e.g., ":8443").
//
@ -104,7 +179,7 @@ func CreateMtlsClient(
// 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,
func ServeWithPredicate(source *workloadapi.X509Source,
initializeRoutes func(),
predicate func(string) bool,
tlsPort string) error {
@ -128,3 +203,35 @@ func Serve(source *workloadapi.X509Source,
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.
//
// 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:
// - error: Returns nil if the server starts successfully, otherwise returns
// an error explaining the failure. Specific error cases include:
// - If source is nil
// - If server creation fails
// - 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) error {
return ServeWithPredicate(
source, initializeRoutes,
func(string) bool { return true }, tlsPort)
}

View File

@ -13,6 +13,8 @@ import (
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
var ErrBadRequest = errors.New("bad request")
var ErrNotReady = errors.New("not ready")
func body(r *http.Response) (bod []byte, err error) {
body, err := io.ReadAll(r.Body)
@ -84,6 +86,15 @@ func Post(client *http.Client, path string, mr []byte) ([]byte, error) {
return []byte{}, ErrUnauthorized
}
if r.StatusCode == http.StatusBadRequest {
return []byte{}, ErrBadRequest
}
// SPIKE Nexus is likely not initialized or in bad shape:
if r.StatusCode == http.StatusServiceUnavailable {
return []byte{}, ErrNotReady
}
return []byte{}, errors.New("post: Problem connecting to peer")
}

29
qodana.yaml Normal file
View File

@ -0,0 +1,29 @@
#-------------------------------------------------------------------------------#
# 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: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#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> #(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

25
retry/mock/mock.go Normal file
View File

@ -0,0 +1,25 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package mock
import (
"context"
)
// MockRetrier implements Retrier for testing
type MockRetrier struct {
RetryFunc func(context.Context, func() error) error
}
// RetryWithBackoff implements the Retrier interface
func (m *MockRetrier) RetryWithBackoff(
ctx context.Context,
operation func() error,
) error {
if m.RetryFunc != nil {
return m.RetryFunc(ctx, operation)
}
return nil
}

251
retry/retry.go Normal file
View File

@ -0,0 +1,251 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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"
"time"
"github.com/cenkalti/backoff/v4"
)
// Default configuration values for the exponential backoff strategy
const (
// Initial wait time between retries
defaultInitialInterval = 500 * time.Millisecond
// Maximum wait time between retries
defaultMaxInterval = 3 * time.Second
// Maximum total time for all retry attempts
defaultMaxElapsedTime = 30 * time.Second
// 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 backoff strategy.
// It will repeatedly execute the operation until it succeeds or
// the context is cancelled. 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 attempt failed
//
// Returns:
// - error: Last error from the operation or nil if successful
RetryWithBackoff(ctx context.Context, op func() error) error
}
// 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.
//
// Example:
//
// retrier := NewTypedRetrier[string](NewExponentialRetrier())
// result, err := retrier.RetryWithBackoff(ctx, func() (string, error) {
// return callExternalService()
// })
func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] {
return &TypedRetrier[T]{retrier: r}
}
// RetryWithBackoff executes a typed operation with 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
// - error: Last error from the operation or nil if successful
func (r *TypedRetrier[T]) RetryWithBackoff(
ctx context.Context,
op func() (T, error),
) (T, error) {
var result T
err := r.retrier.RetryWithBackoff(ctx, func() error {
var err error
result, err = op()
return err
})
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 error, 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.
//
// Example:
//
// retrier := NewExponentialRetrier(
// WithBackOffOptions(
// WithInitialInterval(100 * time.Millisecond),
// WithMaxInterval(5 * time.Second),
// ),
// WithNotify(func(err error, 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
func (r *ExponentialRetrier) RetryWithBackoff(
ctx context.Context,
operation func() error,
) error {
b := r.newBackOff()
totalDuration := time.Duration(0)
return backoff.RetryNotify(
operation,
backoff.WithContext(b, ctx),
func(err error, duration time.Duration) {
totalDuration += duration
if r.notify != nil {
r.notify(err, duration, totalDuration)
}
},
)
}
// WithBackOffOptions configures the backoff settings using the provided
// options. Multiple options can be combined to customize the retry behavior.
//
// 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.
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.s
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.
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.
func WithMultiplier(m float64) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.Multiplier = m
}
}
// WithNotify is an option to set the notification callback.
// The callback is called after each failed attempt with retry statistics.
//
// Example:
//
// retrier := NewExponentialRetrier(
// WithNotify(func(err error, 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, error)
// Do provides a simplified way to retry a typed operation with default
// settings.
// It creates a TypedRetrier with default exponential backoff configuration.
//
// Example:
//
// result, err := Do(ctx, func() (string, error) {
// return fetchData()
// })
func Do[T any](
ctx context.Context,
handler Handler[T],
options ...RetrierOption,
) (T, error) {
return NewTypedRetrier[T](
NewExponentialRetrier(options...),
).RetryWithBackoff(ctx, handler)
}

187
retry/retry_test.go Normal file
View File

@ -0,0 +1,187 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package retry
import (
"context"
"errors"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/stretchr/testify/assert"
)
func TestExponentialRetrier_Success(t *testing.T) {
retrier := NewExponentialRetrier()
// Operation that succeeds immediately
err := retrier.RetryWithBackoff(context.Background(), func() error {
return nil
})
assert.NoError(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() error {
attempts++
if attempts < maxAttempts {
return errors.New("temporary error")
}
return nil
})
assert.NoError(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 := errors.New("persistent error")
err := retrier.RetryWithBackoff(context.Background(), func() error {
return expectedErr
})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
}
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() error {
return errors.New("keep retrying")
})
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
}
func TestExponentialRetrier_Notification(t *testing.T) {
var notifications []time.Duration
totalDurations := make([]time.Duration, 0)
retrier := NewExponentialRetrier(
WithNotify(func(_ error, duration, totalDuration time.Duration) {
notifications = append(notifications, duration)
totalDurations = append(totalDurations, totalDuration)
}),
WithBackOffOptions(
WithInitialInterval(1*time.Millisecond),
WithMaxInterval(5*time.Millisecond),
WithMaxElapsedTime(20*time.Millisecond),
),
)
attempts := 0
_ = retrier.RetryWithBackoff(context.Background(), func() error {
attempts++
if attempts < 3 {
return errors.New("temporary error")
}
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, error) {
return expected, nil
})
assert.NoError(t, err)
assert.Equal(t, expected, result)
}
func TestTypedRetrier_Failure(t *testing.T) {
baseRetrier := NewExponentialRetrier(
WithBackOffOptions(
WithMaxElapsedTime(10*time.Millisecond),
WithInitialInterval(1*time.Millisecond),
),
)
typedRetrier := NewTypedRetrier[int](baseRetrier)
expectedErr := errors.New("typed error")
result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (int, error) {
return 0, expectedErr
})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Equal(t, 0, result)
}
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)
}

195
security/mem/secure.go Normal file
View File

@ -0,0 +1,195 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
// Package mem provides utilities for secure mem operations.
package mem
import (
"crypto/rand"
"runtime"
"syscall"
"unsafe"
)
// ClearRawBytes securely erases all bytes in the provided value by overwriting
// its mem with zeros. This ensures sensitive data like root keys and
// Shamir shards are properly cleaned from mem 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.
//
// Parameters:
// - s: A pointer to any type of data that should be securely erased
//
// Usage:
//
// type SensitiveData struct {
// Key [32]byte
// Token string
// }
//
// data := &SensitiveData{...}
// defer mem.Clear(data)
// // Use data...
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
// employed
// 3. The data being protected is critically sensitive (e.g., high-value
// encryption keys)
//
// For most applications, the standard Clear() 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) {
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 {
panic("")
return
}
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)
}
// 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
if runtime.GOOS == "windows" {
return false
}
// Attempt to lock all current and future memory
if err := syscall.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil {
return false
}
return true
}

View File

@ -0,0 +1,76 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ 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
Clear(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)
}
}
}

150
spiffe/spiffe.go Normal file
View File

@ -0,0 +1,150 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffe
import (
"context"
"errors"
"log"
"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"
)
// 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("SPIFFE_ENDPOINT_SOCKET")
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 function takes a context for cancellation and timeout control, and a
// socket path string specifying the Workload API endpoint location.
//
// It returns:
// - An X509Source that can be used to fetch and monitor X.509 SVIDs
// - The string representation of the current SVID ID
// - An error if the source creation or initial SVID fetch fails
//
// The returned X509Source should be closed when no longer needed.
func Source(ctx context.Context, socketPath string) (
*workloadapi.X509Source, string, error,
) {
source, err := workloadapi.NewX509Source(ctx,
workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)))
if err != nil {
return nil, "", errors.Join(
errors.New("failed to create X509Source"),
err,
)
}
svid, err := source.GetX509SVID()
if err != nil {
return nil, "", errors.Join(
errors.New("unable to get X509SVID"),
err,
)
}
return source, svid.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.
//
// Params:
//
// r *http.Request - 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.
// error - An error object indicating the failure reason. Possible errors
// include the absence of peer certificates or a failure in extracting the
// SPIFFE ID from the certificate.
//
// 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.
func IdFromRequest(r *http.Request) (*spiffeid.ID, error) {
tlsConnectionState := r.TLS
if len(tlsConnectionState.PeerCertificates) == 0 {
return nil, errors.New("no peer certs")
}
id, err := x509svid.IDFromCert(tlsConnectionState.PeerCertificates[0])
if err != nil {
return nil, errors.Join(
err,
errors.New("problem extracting svid"),
)
}
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 and logs any errors that occur during closure without failing.
//
// Parameters:
// - source: The X509Source to close, may be nil
//
// If an error occurs during closure, it will be logged but will not cause the
// function to panic or return an error.
func CloseSource(source *workloadapi.X509Source) {
if source == nil {
return
}
if err := source.Close(); err != nil {
log.Printf("Unable to close X509Source: %v", err)
}
}

272
spiffeid/auth.go Normal file
View File

@ -0,0 +1,272 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffeid
import "strings"
// 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://<trustRoot>/spike/pilot"
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base pilot identity.
//
// Parameters:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - id: 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("example.org,other.org", baseId) {
// // Handle pilot-specific logic
// }
//
// if IsPilot("example.org,other.org", extendedId) {
// // Also recognized as a pilot, with instance metadata
// }
func IsPilot(trustRoots, id string) bool {
for _, root := range strings.Split(trustRoots, ",") {
baseId := SpikePilot(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if id == baseId || strings.HasPrefix(id, 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://<trustRoot>/spike/pilot/recover"
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/recover/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base pilot recovery identity.
//
// Parameters:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - id: 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("example.org,other.org", baseId) {
// // Handle recovery-specific logic
// }
//
// if IsPilotRecover("example.org,other.org", extendedId) {
// // Also recognized as a pilot recovery, with instance metadata
// }
func IsPilotRecover(trustRoots, id string) bool {
for _, root := range strings.Split(trustRoots, ",") {
baseId := SpikePilotRecover(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if id == baseId || strings.HasPrefix(id, 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://<trustRoot>/spike/pilot/restore"
// - Extended match with metadata: "spiffe://<trustRoot>/spike/pilot/restore/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base pilot restore identity.
//
// Parameters:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - id: 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("example.org,other.org", baseId) {
// // Handle restore-specific logic
// }
//
// if IsPilotRestore("example.org,other.org", extendedId) {
// // Also recognized as a pilot restore, with instance metadata
// }
func IsPilotRestore(trustRoots, id string) bool {
for _, root := range strings.Split(trustRoots, ",") {
baseId := SpikePilotRestore(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if id == baseId || strings.HasPrefix(id, 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://<trustRoot>/spike/keeper"
// - Extended match with metadata: "spiffe://<trustRoot>/spike/keeper/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base keeper identity.
//
// Parameters:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - id: 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("example.org", baseId) {
// // Handle keeper-specific logic
// }
//
// if IsKeeper("example.org", extendedId) {
// // Also recognized as a keeper, with instance metadata
// }
func IsKeeper(trustRoots, id string) bool {
for _, root := range strings.Split(trustRoots, ",") {
baseId := SpikeKeeper(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if id == baseId || strings.HasPrefix(id, 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://<trustRoot>/spike/nexus"
// - Extended match with metadata: "spiffe://<trustRoot>/spike/nexus/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base Nexus identity.
//
// Parameters:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - id: 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("example.org", baseId) {
// // Handle Nexus-specific logic
// }
//
// if IsNexus("example.org", extendedId) {
// // Also recognized as a Nexus, with instance metadata
// }
func IsNexus(trustRoots, id string) bool {
for _, root := range strings.Split(trustRoots, ",") {
baseId := SpikeNexus(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if id == baseId || strings.HasPrefix(id, 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:
// - trustRoots: Comma-delimited list of trust domain roots
// (e.g., "example.org,other.org")
// - peerSpiffeId: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the SPIFFE ID matches SPIKE Nexus' SPIFFE ID for any of
// the trust roots, false otherwise
func PeerCanTalkToKeeper(trustRoots, peerSpiffeId string) bool {
return IsNexus(trustRoots, peerSpiffeId)
}

259
spiffeid/auth_test.go Normal file
View File

@ -0,0 +1,259 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffeid
import (
"os"
"testing"
)
func TestIsPilot(t *testing.T) {
tests := []struct {
name string
beforeTest func()
spiffeid string
want bool
}{
{
name: "default valid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/pilot/role/superuser",
want: true,
},
{
name: "default invalid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://test/spike/pilot/role/superuser",
want: false,
},
{
name: "custom valid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://corp.com/spike/pilot/role/superuser",
want: true,
},
{
name: "custom invalid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://invalid/spike/pilot/role/superuser",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.beforeTest != nil {
tt.beforeTest()
}
if got := IsPilot(tt.spiffeid); got != tt.want {
t.Errorf("IsPilot() = %v, want %v", got, tt.want)
}
})
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
panic("failed to unset env SPIKE_TRUST_ROOT")
}
}
}
func TestIsKeeper(t *testing.T) {
tests := []struct {
name string
beforeTest func()
spiffeid string
want bool
}{
{
name: "default valid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/keeper",
want: true,
},
{
name: "default invalid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://test/spike/keeper",
want: false,
},
{
name: "custom valid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://corp.com/spike/keeper",
want: true,
},
{
name: "custom invalid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://invalid/spike/keeper",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.beforeTest != nil {
tt.beforeTest()
}
if got := IsKeeper(tt.spiffeid); got != tt.want {
t.Errorf("IsKeeper() = %v, want %v", got, tt.want)
}
})
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
panic("failed to unset env SPIKE_TRUST_ROOT")
}
}
}
func TestIsNexus(t *testing.T) {
tests := []struct {
name string
beforeTest func()
spiffeid string
want bool
}{
{
name: "default valid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/nexus",
want: true,
},
{
name: "default invalid spiffeid",
beforeTest: nil,
spiffeid: "spiffe://test/spike/nexus",
want: false,
},
{
name: "custom valid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://corp.com/spike/nexus",
want: true,
},
{
name: "custom invalid spiffeid",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://invalid/spike/nexus",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.beforeTest != nil {
tt.beforeTest()
}
if got := IsNexus(tt.spiffeid); got != tt.want {
t.Errorf("IsNexus() = %v, want %v", got, tt.want)
}
})
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
panic("failed to unset env SPIKE_TRUST_ROOT")
}
}
}
func TestCanTalkToAnyone(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{
name: "default",
in: "",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := PeerCanTalkToAnyone(tt.in); got != tt.want {
t.Errorf("PeerCanTalkToAnyone() = %v, want %v", got, tt.want)
}
})
}
}
func TestCanTalkToKeeper(t *testing.T) {
tests := []struct {
name string
beforeTest func()
spiffeid string
want bool
}{
{
name: "default nexus spiffe id",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/nexus",
want: true,
},
{
name: "default keeper spiffe id",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/keeper",
// Keepers cannot talk to keepers.
want: false,
},
{
name: "custom nexus spiffe id",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://corp.com/spike/nexus",
want: true,
},
{
name: "custom keeper spiffe id",
beforeTest: func() {
if err := os.Setenv("SPIKE_TRUST_ROOT", "corp.com"); err != nil {
panic("failed to set env SPIKE_TRUST_ROOT")
}
},
spiffeid: "spiffe://corp.com/spike/keeper",
// Keepers cannot talk to keepers; only Nexus can talk to Keepers.
want: false,
},
{
name: "pilot spiffe id",
beforeTest: nil,
spiffeid: "spiffe://spike.ist/spike/pilot/role/superuser",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.beforeTest != nil {
tt.beforeTest()
}
if got := PeerCanTalkToKeeper(tt.spiffeid); got != tt.want {
t.Errorf("PeerCanTalkToKeeper() = %v, want %v", got, tt.want)
}
})
if err := os.Unsetenv("SPIKE_TRUST_ROOT"); err != nil {
panic("failed to unset env SPIKE_TRUST_ROOT")
}
}
}

View File

@ -2,9 +2,14 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package data
package env
type InitState string
import "os"
const AlreadyInitialized InitState = "AlreadyInitialized"
const NotInitialized InitState = "NotInitialized"
func TrustRoot() string {
tr := os.Getenv("SPIKE_TRUST_ROOT")
if tr == "" {
return "spike.ist"
}
return tr
}

96
spiffeid/spiffeid.go Normal file
View File

@ -0,0 +1,96 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffeid
import (
"path"
"github.com/spiffe/spike-sdk-go/spiffeid/internal/env"
)
// SpikeKeeper 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://<trustRoot>/spike/keeper"
func SpikeKeeper(trustRoot string) string {
if trustRoot == "" {
trustRoot = env.TrustRoot()
}
return "spiffe://" + path.Join(trustRoot, "spike", "keeper")
}
// SpikeNexus 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://<trustRoot>/spike/nexus"
func SpikeNexus(trustRoot string) string {
if trustRoot == "" {
trustRoot = env.TrustRoot()
}
return "spiffe://" + path.Join(trustRoot, "spike", "nexus")
}
// SpikePilot 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://<trustRoot>/spike/pilot/role/superuser"
func SpikePilot(trustRoot string) string {
if trustRoot == "" {
trustRoot = env.TrustRoot()
}
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "superuser")
}
// SpikePilotRecover 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://<trustRoot>/spike/pilot/role/recover"
func SpikePilotRecover(trustRoot string) string {
if trustRoot == "" {
trustRoot = env.TrustRoot()
}
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "recover")
}
// SpikePilotRestore 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://<trustRoot>/spike/pilot/role/restore"
func SpikePilotRestore(trustRoot string) string {
if trustRoot == "" {
trustRoot = env.TrustRoot()
}
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "restore")
}

46
system/system.go Normal file
View File

@ -0,0 +1,46 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package system
import (
"log"
"os"
"os/signal"
"syscall"
)
// KeepAlive blocks the current goroutine until it receives either a
// SIGINT (Ctrl+C) or SIGTERM signal, enabling graceful shutdown of the
// application. Upon receiving a termination signal, it logs the signal type
// and begins the shutdown process.
//
// 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.
//
// 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.
//
// Example usage:
//
// func main() {
// // Initialize your application
// setupApp()
//
// // Keep the application running until shutdown signal
// KeepAlive()
//
// // Perform cleanup
// cleanup()
// }
func KeepAlive() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigChan
log.Printf("\nReceived %v signal, shutting down gracefully...\n", sig)
}

123
validation/validation.go Normal file
View File

@ -0,0 +1,123 @@
package validation
import (
"regexp"
"github.com/google/uuid"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/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.
// It returns an error if the name is invalid, otherwise it returns nil.
func ValidateName(name string) error {
// Validate length
if len(name) == 0 || len(name) > maxNameLength {
return errors.ErrInvalidInput
}
// Validate format
if match, _ := regexp.MatchString(validNamePattern, name); !match {
return errors.ErrInvalidInput
}
return nil
}
// ValidateSpiffeIdPattern validates whether the given SPIFFE ID pattern string
// conforms to the expected format and returns an error if it does not.
func ValidateSpiffeIdPattern(spiffeIdPattern string) error {
// Validate SpiffeIdPattern
if match, _ := regexp.MatchString(
validSpiffeIdPattern, spiffeIdPattern); !match {
return errors.ErrInvalidInput
}
return nil
}
// ValidateSpiffeId validates if the given SPIFFE ID matches the expected format.
// Returns an error if the SPIFFE ID is invalid.
func ValidateSpiffeId(spiffeId string) error {
if match, _ := regexp.MatchString(
validRawSpiffeIdPattern, spiffeId); !match {
return errors.ErrInvalidInput
}
return nil
}
// ValidatePathPattern validates the given path pattern string for correctness.
// Returns an error if the pattern is empty, too long, or has invalid characters.
func ValidatePathPattern(pathPattern string) error {
// Validate length
if len(pathPattern) == 0 || len(pathPattern) > maxPathPatternLength {
return errors.ErrInvalidInput
}
// Validate format
// Allow regex special characters along with alphanumeric and basic symbols
if match, _ := regexp.MatchString(validPathPattern, pathPattern); !match {
return errors.ErrInvalidInput
}
return nil
}
// ValidatePath checks if the given path is valid based on predefined rules.
// It returns an error if the path is empty, too long, or contains invalid
// characters.
func ValidatePath(path string) error {
if len(path) == 0 || len(path) > maxPathPatternLength {
return errors.ErrInvalidInput
}
if match, _ := regexp.MatchString(validPath, path); !match {
return errors.ErrInvalidInput
}
return nil
}
// ValidatePolicyId verifies if the given policyId is a valid UUID format.
// Returns errors.ErrInvalidInput if the validation fails.
func ValidatePolicyId(policyId string) error {
err := uuid.Validate(policyId)
if err != nil {
return errors.ErrInvalidInput
}
return nil
}
// ValidatePermissions checks if all provided permissions are valid.
// Permissions are compared against a predefined list of allowed permissions.
// Returns ErrInvalidInput if any permission is invalid, nil otherwise.
func ValidatePermissions(permissions []data.PolicyPermission) error {
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 errors.ErrInvalidInput
}
}
return nil
}