Compare commits

..

No commits in common. "main" and "v0.2.7" have entirely different histories.
main ... v0.2.7

43 changed files with 230 additions and 2502 deletions

View File

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

View File

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

View File

@ -8,7 +8,6 @@ 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"`
@ -16,50 +15,41 @@ 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

@ -0,0 +1,29 @@
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
)
// 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,7 +7,8 @@ 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 {
Shard *[32]byte `json:"shard"`
KeeperId string `json:"id"`
Shard string `json:"shard"`
}
// ShardContributionResponse represents the response structure for a shard
@ -23,6 +24,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 *[32]byte `json:"shard"`
Shard string `json:"shard"`
Err data.ErrorCode
}

View File

@ -15,5 +15,3 @@ 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,3 +30,34 @@ 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:
// - map[int]*[32]byte: Map of shard indices to shard byte arrays if
// successful, nil if not found
// - *[]string: Array of recovery shard identifiers 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) (map[int]*[32]byte, error) {
func Recover(source *workloadapi.X509Source) (*[]string, error) {
r := reqres.RecoverRequest{}
mr, err := json.Marshal(r)
@ -70,11 +70,5 @@ func Recover(source *workloadapi.X509Source) (map[int]*[32]byte, error) {
return nil, errors.New(string(res.Err))
}
result := make(map[int]*[32]byte)
for i, shard := range res.Shards {
result[i] = shard
}
return result, nil
return &res.Shards, nil
}

View File

@ -7,27 +7,24 @@ 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 *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
// - source: X509Source used for mTLS client authentication
// - shard: Recovery shard identifier to submit
//
// Returns:
// - *data.RestorationStatus: Status containing shards collected, remaining,
// and restoration state if successful, nil if not found
// - *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
@ -37,21 +34,13 @@ import (
//
// Example:
//
// status, err := Restore(x509Source, shardIndex, shardValue)
// status, err := Restore(x509Source, "randomshardentry")
func Restore(
source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte,
source *workloadapi.X509Source, shard string,
) (*data.RestorationStatus, error) {
r := reqres.RestoreRequest{
Id: shardIndex,
Shard: shardValue,
}
r := reqres.RestoreRequest{Shard: shard}
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"),
@ -61,19 +50,10 @@ 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,17 +4,21 @@
package url
type ApiAction string
type SpikeNexusApiAction string
const KeyApiAction = "action"
const keyApiAction = "action"
const ActionCheck ApiAction = "check"
const ActionGet ApiAction = "get"
const ActionDelete ApiAction = "delete"
const ActionUndelete ApiAction = "undelete"
const ActionList ApiAction = "list"
const ActionDefault ApiAction = ""
const ActionRead ApiAction = "read"
const actionNexusCheck SpikeNexusApiAction = "check"
const actionNexusGet SpikeNexusApiAction = "get"
const actionNexusDelete SpikeNexusApiAction = "delete"
const actionNexusUndelete SpikeNexusApiAction = "undelete"
const actionNexusList SpikeNexusApiAction = "list"
const actionNexusDefault SpikeNexusApiAction = ""
type SpikeKeeperApiAction string
const actionKeeperRead SpikeKeeperApiAction = "read"
const actionKeeperDefault SpikeKeeperApiAction = ""
type ApiUrl string
@ -24,9 +28,13 @@ const SpikeNexusUrlInit ApiUrl = "/v1/auth/initialization"
const SpikeNexusUrlPolicy ApiUrl = "/v1/acl/policy"
const SpikeNexusUrlOperatorRecover ApiUrl = "/v1/operator/recover"
const SpikeNexusUrlOperatorRestore ApiUrl = "/v1/operator/restore"
const SpikeNexusUrlRecover ApiUrl = "/v1/operator/recover"
const SpikeNexusUrlRestore 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(SpikeNexusUrlOperatorRestore),
string(SpikeNexusUrlRestore),
)
return u
}
@ -21,7 +21,7 @@ func Restore() string {
func Recover() string {
u, _ := url.JoinPath(
env.NexusApiRoot(),
string(SpikeNexusUrlOperatorRecover),
string(SpikeNexusUrlRecover),
)
return u
}

View File

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

View File

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

View File

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

25
go.mod
View File

@ -1,28 +1,27 @@
module github.com/spiffe/spike-sdk-go
go 1.24
toolchain go1.24.1
go 1.23.3
require (
github.com/cenkalti/backoff/v4 v4.3.0
github.com/google/uuid v1.6.0
github.com/spiffe/go-spiffe/v2 v2.5.0
github.com/stretchr/testify v1.10.0
github.com/spiffe/go-spiffe/v2 v2.4.0
github.com/stretchr/testify v1.9.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.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/grpc v1.72.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
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
gopkg.in/yaml.v3 v3.0.1 // indirect
)

151
go.sum
View File

@ -1,169 +1,42 @@
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/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/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/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/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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
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=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/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=

View File

@ -1,10 +0,0 @@
#!/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

View File

@ -1,54 +0,0 @@
// \\ 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
}

View File

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

View File

@ -1,9 +0,0 @@
// \\ 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

View File

@ -1,56 +0,0 @@
// \\ 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
}

View File

@ -1,14 +0,0 @@
// \\ 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")
)

View File

@ -1,80 +0,0 @@
// \\ 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
}

View File

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

View File

@ -1,64 +0,0 @@
// \\ 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
}
}

View File

@ -1,24 +0,0 @@
// \\ 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),
}
}

View File

@ -1,21 +0,0 @@
// \\ 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
}

View File

@ -1,61 +0,0 @@
package kv
import "testing"
func TestKV_List(t *testing.T) {
tests := []struct {
name string
setup func() *KV
want []string
}{
{
name: "empty_store",
setup: func() *KV {
return &KV{
data: make(map[string]*Value),
}
},
want: []string{},
},
{
name: "non_empty_store",
setup: func() *KV {
return &KV{
data: map[string]*Value{
"test/path": {
Metadata: Metadata{
CurrentVersion: 1,
},
Versions: map[int]Version{
1: {
Data: map[string]string{
"key": "value",
},
Version: 1,
},
},
},
},
}
},
want: []string{"test/path"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kv := tt.setup()
got := kv.List()
if len(got) != len(tt.want) {
t.Errorf("got %v want %v", got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("got %v want %v", got, tt.want)
}
}
})
}
}

View File

@ -1,85 +0,0 @@
// \\ 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
}
}

View File

@ -1,94 +0,0 @@
package kv
import (
"testing"
)
func TestKV_Put(t *testing.T) {
tests := []struct {
name string
setup func() *KV
path string
values map[string]string
versions []int
wantErr error
}{
{
setup: func() *KV {
return &KV{
data: make(map[string]*Value),
maxSecretVersions: 10,
}
},
name: "it creates a new secret with initial metadata if the path doesn't exist",
path: "new/secret/path",
versions: []int{1},
values: map[string]string{"key": "value"},
wantErr: nil,
},
{
name: "it creates a new version with an incremented version number",
setup: func() *KV {
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10}
kv.Put("existing/secret/path", map[string]string{"key": "value1"})
return kv
},
path: "existing/secret/path",
versions: []int{1, 2},
wantErr: nil,
},
{
name: "it automatically prunes old versions when exceeding MaxVersions",
setup: func() *KV {
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 2}
kv.Put("prune/old/versions", map[string]string{"key": "value1"})
kv.Put("prune/old/versions", map[string]string{"key": "value2"})
kv.Put("prune/old/versions", map[string]string{"key": "value3"})
return kv
},
path: "prune/old/versions",
versions: []int{4, 3},
values: map[string]string{
"key": "value4",
},
wantErr: nil,
},
{
name: "it updates timestamps for both creation and modification times",
setup: func() *KV {
kv := &KV{data: make(map[string]*Value), maxSecretVersions: 10}
kv.Put("update/timestamps", map[string]string{"key": "value1"})
return kv
},
versions: []int{1, 2},
path: "update/timestamps",
values: map[string]string{"key": "value2"},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kv := tt.setup()
kv.Put(tt.path, tt.values)
secret, exists := kv.data[tt.path]
if !exists {
t.Fatalf("expected secret to exist at path %q", tt.path)
}
if len(secret.Versions) != len(tt.versions) {
t.Fatalf("expected %d versions, got %d", len(tt.versions), len(secret.Versions))
}
for _, version := range tt.versions {
if _, exists := secret.Versions[version]; !exists {
t.Fatalf("expected version %d to exist", version)
}
}
if tt.wantErr != nil {
t.Fatalf("unexpected error: %v", tt.wantErr)
}
})
}
}

View File

@ -1,57 +0,0 @@
// \\ 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
}

View File

@ -1,182 +0,0 @@
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,32 +7,13 @@ 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
@ -114,19 +95,7 @@ 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,8 +13,6 @@ 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)
@ -86,15 +84,6 @@ 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")
}

View File

@ -1,29 +0,0 @@
#-------------------------------------------------------------------------------#
# 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,8 +231,7 @@ 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:
@ -240,12 +239,8 @@ 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],
options ...RetrierOption,
) (T, error) {
func Do[T any](ctx context.Context, handler Handler[T]) (T, error) {
return NewTypedRetrier[T](
NewExponentialRetrier(options...),
NewExponentialRetrier(),
).RetryWithBackoff(ctx, handler)
}

View File

@ -1,195 +0,0 @@
// \\ 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

@ -1,76 +0,0 @@
// \\ 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)
}
}
}

View File

@ -1,272 +0,0 @@
// \\ 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)
}

View File

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

View File

@ -1,15 +0,0 @@
// \\ 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
}

View File

@ -1,96 +0,0 @@
// \\ 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")
}