Compare commits

...

55 Commits
v0.2.7 ... 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
43 changed files with 2502 additions and 230 deletions

View File

@ -49,11 +49,12 @@ func (a *Api) Close() {
// to the server.
//
// The function takes the following parameters:
// - name: The name of the policy to be created
// - spiffeIdPattern: The SPIFFE ID pattern that this policy will apply to
// - pathPattern: The path pattern that this policy will match against
// - permissions: A slice of PolicyPermission defining the access rights for
// this policy
// - 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
@ -71,7 +72,7 @@ func (a *Api) Close() {
// },
// }
//
// err = CreatePolicy(
// err = api.CreatePolicy(
// "doc-reader",
// "spiffe://example.org/service/*",
// "/api/documents/*",
@ -89,10 +90,10 @@ func (a *Api) CreatePolicy(
name, spiffeIdPattern, pathPattern, permissions)
}
// DeletePolicy removes an existing policy from the system using its Id.
// DeletePolicy removes an existing policy from the system using its name.
//
// The function takes the following parameters:
// - id: The unique identifier of the policy to be deleted
// - 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
@ -103,7 +104,7 @@ func (a *Api) CreatePolicy(
//
// Example usage:
//
// err = DeletePolicy("policy-123")
// err = api.DeletePolicy("doc-reader")
// if err != nil {
// log.Printf("Failed to delete policy: %v", err)
// return
@ -112,10 +113,10 @@ func (a *Api) DeletePolicy(name string) error {
return acl.DeletePolicy(a.source, name)
}
// GetPolicy retrieves a policy from the system using its Id.
// GetPolicy retrieves a policy from the system using its name.
//
// The function takes the following parameters:
// - id: The unique identifier of the policy to retrieve
// - name string: The name of the policy to retrieve
//
// The function returns:
// - (*data.Policy, nil) if the policy is found
@ -131,7 +132,7 @@ func (a *Api) DeletePolicy(name string) error {
//
// Example usage:
//
// policy, err := GetPolicy("policy-123")
// policy, err := api.GetPolicy("doc-reader")
// if err != nil {
// log.Printf("Error retrieving policy: %v", err)
// return
@ -166,13 +167,7 @@ func (a *Api) GetPolicy(name string) (*data.Policy, error) {
//
// Example usage:
//
// source, err := workloadapi.NewX509Source(context.Background())
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// result, err := ListPolicies(source)
// result, err := api.ListPolicies()
// if err != nil {
// log.Printf("Error listing policies: %v", err)
// return
@ -193,13 +188,11 @@ func (a *Api) ListPolicies() (*[]data.Policy, error) {
// DeleteSecretVersions deletes specified versions of a secret at the given
// path
//
// It converts string version numbers to integers, constructs a delete request,
// and sends it to the secrets API endpoint. If no versions are specified or
// conversion fails, no versions will be deleted.
// It constructs a delete request and sends it to the secrets API endpoint.
//
// Parameters:
// - path: Path to the secret to delete
// - versions: String array of version numbers to delete
// - 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
@ -207,20 +200,15 @@ func (a *Api) ListPolicies() (*[]data.Policy, error) {
//
// Example:
//
// err := DeleteSecretVersions("secret/path", []string{"1", "2"})
// 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 specified secret at the given path
//
// It converts string version numbers to integers, constructs a delete request,
// and sends it to the secrets API endpoint. If no versions are specified or
// conversion fails, no versions will be deleted.
// DeleteSecret deletes the entire secret at the given path
//
// Parameters:
// - path: Path to the secret to delete
// - versions: String array of version numbers to delete
// - path string: Path to the secret to delete
//
// Returns:
// - error: nil on success, unauthorized error if not logged in, or wrapped
@ -228,7 +216,7 @@ func (a *Api) DeleteSecretVersions(path string, versions []int) error {
//
// Example:
//
// err := Delete("secret/path")
// err := api.DeleteSecret("secret/path")
func (a *Api) DeleteSecret(path string) error {
return secret.Delete(a.source, path, []int{})
}
@ -237,36 +225,36 @@ func (a *Api) DeleteSecret(path string) error {
// path.
//
// Parameters:
// - path: Path to the secret to retrieve
// - version: Version number of the secret to retrieve
// - path string: Path to the secret to retrieve
// - version int: Version number of the secret to retrieve
//
// Returns:
// - *Secret: Secret data if found, nil if secret not found
// - *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 := GetSecretVersion("secret/path", 1)
// 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 secret at the given path.
// GetSecret retrieves the latest version of the secret at the given path.
//
// Parameters:
// - path: Path to the secret to retrieve
// - path string: Path to the secret to retrieve
//
// Returns:
// - *Secret: Secret data if found, nil if secret not found
// - *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 := Get("secret/path")
// secret, err := api.GetSecret("secret/path")
func (a *Api) GetSecret(path string) (*data.Secret, error) {
return secret.Get(a.source, path, 0)
}
@ -274,32 +262,32 @@ func (a *Api) GetSecret(path string) (*data.Secret, error) {
// ListSecretKeys retrieves all secret keys.
//
// Returns:
// - []string: Array of secret keys if found, empty array if none found
// - *[]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 := ListKeys()
// keys, err := api.ListSecretKeys()
func (a *Api) ListSecretKeys() (*[]string, error) {
return secret.ListKeys(a.source)
}
// GetSecretMetadata retrieves a specific version of a secret metadata at the
// given path.
// GetSecretMetadata retrieves metadata for a specific version of a secret at
// the given path.
//
// Parameters:
// - path: Path to the secret to retrieve
// - version: Version number of the secret to retrieve
// - path string: Path to the secret to retrieve metadata for
// - version int: Version number of the secret to retrieve metadata for
//
// Returns:
// - *Secret: Secret metadata if found, nil if secret not found
// - *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 := GetMetadata("secret/path", 1)
// metadata, err := api.GetSecretMetadata("secret/path", 1)
func (a *Api) GetSecretMetadata(
path string, version int,
) (*data.SecretMetadata, error) {
@ -310,8 +298,9 @@ func (a *Api) GetSecretMetadata(
// values.
//
// Parameters:
// - path: Path where the secret should be stored
// - values: Map of key-value pairs representing the secret data
// - 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
@ -319,7 +308,7 @@ func (a *Api) GetSecretMetadata(
//
// Example:
//
// err := Put("secret/path", map[string]string{"key": "value"})
// 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)
}
@ -328,8 +317,8 @@ func (a *Api) PutSecret(path string, data map[string]string) error {
// specified path.
//
// Parameters:
// - path: Path to the secret to restore
// - versions: String array of version numbers to restore. Empty array
// - path string: Path to the secret to restore
// - versions []int: Array of version numbers to restore. Empty array
// attempts no restoration
//
// Returns:
@ -338,27 +327,27 @@ func (a *Api) PutSecret(path string, data map[string]string) error {
//
// Example:
//
// err := Undelete("secret/path", []string{"1", "2"})
// 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 return recovery partitions for SPIKE Nexus to be used in a
// 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 shared are sensitive and should be securely stored out-of-band
// The returned shards are sensitive and should be securely stored out-of-band
// in encrypted form.
//
// Returns:
// - []string: Array of recovery shards
// - *[][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 := Recover()
func (a *Api) Recover() (*[]string, error) {
// shards, err := api.Recover()
func (a *Api) Recover() (map[int]*[32]byte, error) {
return operator.Recover(a.source)
}
@ -369,16 +358,16 @@ func (a *Api) Recover() (*[]string, error) {
// SPIKE deployment should not need.
//
// Parameters:
// - shard: the shared to seed.
// - shard *[32]byte: Pointer to a 32-byte array containing the shard to seed
//
// Returns:
// - *RestorationStatus: Status of the restoration process if successful
// - *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 := Restore("randomShardString")
func (a *Api) Restore(shard string) (*data.RestorationStatus, error) {
return operator.Restore(a.source, shard)
// 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

@ -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

@ -6,18 +6,24 @@ package reqres
import "github.com/spiffe/spike-sdk-go/api/entity/data"
// RestoreRequest for disaster recovery.
type RestoreRequest struct {
Shard string `json:"shard"`
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 []string `json:"shards"`
Err data.ErrorCode `json:"err,omitempty"`
Shards map[int]*[32]byte `json:"shards"`
Err data.ErrorCode `json:"err,omitempty"`
}

View File

@ -8,6 +8,7 @@ import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// PolicyCreateRequest for policy creation.
type PolicyCreateRequest struct {
Name string `json:"name"`
SpiffeIdPattern string `json:"spiffedPattern"`
@ -15,41 +16,50 @@ type PolicyCreateRequest struct {
Permissions []data.PolicyPermission `json:"permissions"`
}
// PolicyCreateResponse for policy creation.
type PolicyCreateResponse struct {
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 {
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 data.ErrorCode `json:"err,omitempty"`
}
// PolicyListRequest to list policies.
type PolicyListRequest struct{}
// PolicyListResponse to list policies.
type PolicyListResponse struct {
Policies []data.Policy `json:"policies"`
Err data.ErrorCode `json:"err,omitempty"`
}
// PolicyAccessCheckRequest to validate policy access.
type PolicyAccessCheckRequest struct {
SpiffeId string `json:"spiffeId"`
Path string `json:"path"`
Action string `json:"action"`
}
// PolicyAccessCheckResponse to validate policy access,.
type PolicyAccessCheckResponse struct {
Allowed bool `json:"allowed"`
MatchingPolicies []string `json:"matchingPolicies"`

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

@ -7,8 +7,7 @@ import "github.com/spiffe/spike-sdk-go/api/entity/data"
// Shard represents the shard data being contributed to the system.
// Version optionally specifies the version of the shard being submitted.
type ShardContributionRequest struct {
KeeperId string `json:"id"`
Shard string `json:"shard"`
Shard *[32]byte `json:"shard"`
}
// ShardContributionResponse represents the response structure for a shard
@ -24,6 +23,6 @@ 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 string `json:"shard"`
Shard *[32]byte `json:"shard"`
Err data.ErrorCode
}

View File

@ -15,3 +15,5 @@ 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

@ -7,11 +7,11 @@ package operator
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/url"
"github.com/spiffe/spike-sdk-go/net"
)
@ -22,8 +22,8 @@ import (
// - source: X509Source used for mTLS client authentication
//
// Returns:
// - *[]string: Array of recovery shard identifiers if successful, nil if
// not found
// - 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
@ -34,7 +34,7 @@ import (
// Example:
//
// shards, err := Recover(x509Source)
func Recover(source *workloadapi.X509Source) (*[]string, error) {
func Recover(source *workloadapi.X509Source) (map[int]*[32]byte, error) {
r := reqres.RecoverRequest{}
mr, err := json.Marshal(r)
@ -70,5 +70,11 @@ func Recover(source *workloadapi.X509Source) (*[]string, error) {
return nil, errors.New(string(res.Err))
}
return &res.Shards, nil
result := make(map[int]*[32]byte)
for i, shard := range res.Shards {
result[i] = shard
}
return result, nil
}

View File

@ -7,24 +7,27 @@ package operator
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/url"
"github.com/spiffe/spike-sdk-go/net"
)
// Restore submits a recovery shard to continue the restoration process.
//
// Parameters:
// - source: X509Source used for mTLS client authentication
// - shard: Recovery shard identifier to submit
// - 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:
// - *RestorationStatus: Status containing shards collected, remaining, and
// restoration state if successful, nil if not found
// - *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
@ -34,13 +37,21 @@ import (
//
// Example:
//
// status, err := Restore(x509Source, "randomshardentry")
// status, err := Restore(x509Source, shardIndex, shardValue)
func Restore(
source *workloadapi.X509Source, shard string,
source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte,
) (*data.RestorationStatus, error) {
r := reqres.RestoreRequest{Shard: shard}
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"),
@ -50,10 +61,19 @@ func Restore(
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

View File

@ -4,21 +4,17 @@
package url
type SpikeNexusApiAction string
type ApiAction string
const keyApiAction = "action"
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 = ""
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
@ -28,13 +24,9 @@ const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
const SpikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
const SpikeNexusUrlRecover ApiUrl = "/v1/operator/recover"
const SpikeNexusUrlRestore ApiUrl = "/v1/operator/restore"
const SpikeNexusUrlOperatorRecover ApiUrl = "/v1/operator/recover"
const SpikeNexusUrlOperatorRestore ApiUrl = "/v1/operator/restore"
const SpikeKeeperUrlKeep ApiUrl = "/v1/store/keep"
const SpikeNexusUrlOperatorRestore = "/v1/operator/restore"
const SpikeNexusUrlOperatorRecover = "/v1/operator/recover"
const SpikeKeeperUrlContribute ApiUrl = "/v1/store/contribute"
const SpikeKeeperUrlShard ApiUrl = "/v1/store/shard"

View File

@ -13,7 +13,7 @@ import (
func Restore() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(SpikeNexusUrlRestore),
string(SpikeNexusUrlOperatorRestore),
)
return u
}
@ -21,7 +21,7 @@ func Restore() string {
func Recover() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(SpikeNexusUrlRecover),
string(SpikeNexusUrlOperatorRecover),
)
return u
}

View File

@ -26,7 +26,7 @@ func PolicyList() string {
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusList))
params.Add(KeyApiAction, string(ActionList))
return u + "?" + params.Encode()
}
@ -37,7 +37,7 @@ func PolicyDelete() string {
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusDelete))
params.Add(KeyApiAction, string(ActionDelete))
return u + "?" + params.Encode()
}
@ -48,6 +48,6 @@ func PolicyGet() string {
string(SpikeNexusUrlPolicy),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusGet))
params.Add(KeyApiAction, string(ActionGet))
return u + "?" + params.Encode()
}

View File

@ -10,18 +10,18 @@ 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),
)
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(),
@ -30,46 +30,46 @@ func SecretPut() string {
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),
)
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),
)
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),
)
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),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusGet))
params.Add(KeyApiAction, string(ActionGet))
return u + "?" + params.Encode()
}

View File

@ -10,7 +10,7 @@ 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(),
@ -19,7 +19,7 @@ func Init() string {
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(
@ -27,6 +27,6 @@ func InitState() string {
string(SpikeNexusUrlInit),
)
params := url.Values{}
params.Add(keyApiAction, string(actionNexusCheck))
params.Add(KeyApiAction, string(ActionCheck))
return u + "?" + params.Encode()
}

25
go.mod
View File

@ -1,27 +1,28 @@
module github.com/spiffe/spike-sdk-go
go 1.23.3
go 1.24
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.4.0
github.com/stretchr/testify v1.9.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/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // 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.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/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
)

151
go.sum
View File

@ -1,42 +1,169 @@
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=

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
@ -95,7 +114,19 @@ func CreateMtlsClientWithPredicate(
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

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

View File

@ -231,7 +231,8 @@ func WithNotify(fn NotifyFn) RetrierOption {
// 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.
// Do provides a simplified way to retry a typed operation with default
// settings.
// It creates a TypedRetrier with default exponential backoff configuration.
//
// Example:
@ -239,8 +240,12 @@ type Handler[T any] func() (T, error)
// result, err := Do(ctx, func() (string, error) {
// return fetchData()
// })
func Do[T any](ctx context.Context, handler Handler[T]) (T, error) {
func Do[T any](
ctx context.Context,
handler Handler[T],
options ...RetrierOption,
) (T, error) {
return NewTypedRetrier[T](
NewExponentialRetrier(),
NewExponentialRetrier(options...),
).RetryWithBackoff(ctx, handler)
}

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)
}
}
}

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")
}
}
}

15
spiffeid/internal/env/env.go vendored Normal file
View File

@ -0,0 +1,15 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import "os"
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")
}