package api
import (
"github.com/cloudflare/circl/secretsharing"
"github.com/spiffe/spike-sdk-go/api/internal/impl/bootstrap"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// Contribute sends a secret share contribution to a SPIKE Keeper during the
// bootstrap process.
//
// It establishes a mutual TLS connection to the specified Keeper and transmits
// the keeper's share of the secret. The function marshals the share value,
// validates its length, and sends it securely to the Keeper. After sending, the
// contribution is zeroed out in memory for security.
//
// Parameters:
// - keeperShare: The secret share to contribute to the Keeper
// - keeperID: The unique identifier of the target Keeper
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - Errors from net.Post(): if the HTTP request fails
//
// Note: The function will fatally crash (via log.FatalErr) if:
// - Marshal failures (ErrDataMarshalFailure)
// - Share length validation fails (ErrCryptoInvalidEncryptionKeyLength)
func (a *API) Contribute(
keeperShare secretsharing.Share, keeperID string,
) *sdkErrors.SDKError {
return bootstrap.Contribute(a.source, keeperShare, keeperID)
}
// Verify performs bootstrap verification with SPIKE Nexus by sending encrypted
// random text and validating that Nexus can decrypt it correctly.
//
// This ensures that the bootstrap process completed successfully and Nexus has
// the correct master key. The function sends the nonce and ciphertext to Nexus,
// receives back a hash, and compares it against the expected hash of the
// original random text. A match confirms successful bootstrap.
//
// Parameters:
// - randomText: The original random text that was encrypted
// - nonce: The nonce used during encryption
// - cipherText: The encrypted random text
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - Errors from net.Post(): if the HTTP request fails
//
// Note: The function will fatally crash (via log.FatalErr) if:
// - Marshal failures (ErrDataMarshalFailure)
// - Response parsing failures (ErrDataUnmarshalFailure)
// - Hash verification fails (ErrCryptoCipherVerificationFailed)
func (a *API) Verify(
randomText string, nonce, cipherText []byte,
) *sdkErrors.SDKError {
return bootstrap.Verify(a.source, randomText, nonce, cipherText)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"io"
"github.com/spiffe/spike-sdk-go/api/internal/impl/cipher"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// CipherEncryptStream encrypts data from a reader using streaming mode.
//
// It sends the reader content as the request body to SPIKE Nexus for encryption.
// The data is treated as binary (application/octet-stream) regardless of its
// original format, as encryption operates on raw bytes.
//
// Parameters:
// - reader: The data source to encrypt
//
// Returns:
// - []byte: The encrypted ciphertext if successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// reader := strings.NewReader("sensitive data")
// encrypted, err := api.CipherEncryptStream(reader)
func (a *API) CipherEncryptStream(
reader io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return cipher.EncryptStream(a.source, reader)
}
// CipherEncrypt encrypts data with structured parameters.
//
// It sends plaintext and algorithm to SPIKE Nexus and returns the
// encrypted ciphertext bytes.
//
// Parameters:
// - plaintext: The data to encrypt
// - algorithm: The encryption algorithm to use (e.g., "AES-GCM")
//
// Returns:
// - []byte: The encrypted ciphertext if successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// data := []byte("secret message")
// encrypted, err := api.CipherEncrypt(data, "AES-GCM")
func (a *API) CipherEncrypt(
plaintext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
return cipher.Encrypt(a.source, plaintext, algorithm)
}
// CipherDecryptStream decrypts data from a reader using streaming mode.
//
// It sends the reader content as the request body to SPIKE Nexus for decryption.
// The data is treated as binary (application/octet-stream) as decryption
// operates on raw encrypted bytes.
//
// Parameters:
// - reader: The encrypted data source to decrypt
//
// Returns:
// - []byte: The decrypted plaintext if successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// reader := bytes.NewReader(encryptedData)
// plaintext, err := api.CipherDecryptStream(reader)
func (a *API) CipherDecryptStream(
reader io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return cipher.DecryptStream(a.source, reader)
}
// CipherDecrypt decrypts data with structured parameters.
//
// It sends version, nonce, ciphertext, and algorithm to SPIKE Nexus
// and returns the decrypted plaintext.
//
// Parameters:
// - version: The cipher version used during encryption
// - nonce: The nonce bytes used during encryption
// - ciphertext: The encrypted data to decrypt
// - algorithm: The encryption algorithm used (e.g., "AES-GCM")
//
// Returns:
// - []byte: The decrypted plaintext if successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// plaintext, err := api.CipherDecrypt(1, nonce, ciphertext, "AES-GCM")
func (a *API) CipherDecrypt(
version byte, nonce, ciphertext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
return cipher.Decrypt(a.source, version, nonce, ciphertext, algorithm)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/spiffe"
)
// Close releases any resources held by the API instance.
//
// It ensures proper cleanup of the underlying X509Source. This method should
// be called when the API instance is no longer needed, typically during
// application shutdown or cleanup.
//
// Returns:
// - *sdkErrors.SDKError: nil if successful or source is nil,
// ErrSPIFFEFailedToCreateX509Source if closure fails
//
// Example:
//
// api, err := NewAPI(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer func() {
// if err := api.Close(); err != nil {
// log.Printf("Failed to close API: %v", err)
// }
// }()
func (a *API) Close() *sdkErrors.SDKError {
if a.source == nil {
return nil
}
return spiffe.CloseSource(a.source)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/spiffe"
)
// API is the SPIKE API.
type API struct {
source *workloadapi.X509Source
}
// New creates and returns a new instance of API configured with a SPIFFE
// source.
//
// It automatically discovers and connects to the SPIFFE Workload API endpoint
// using the default socket path and creates an X.509 source for authentication
// with a configurable timeout to prevent indefinite blocking on socket issues.
//
// The timeout can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT
// environment variable (default: 30s).
//
// The API client is configured to communicate exclusively with SPIKE Nexus.
//
// Returns:
// - *API: A configured API instance ready for use, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFEFailedToCreateX509Source: if X509Source creation fails
// - ErrSPIFFEUnableToFetchX509Source: if initial SVID fetch fails
//
// Example:
//
// api, err := New()
// if err != nil {
// log.Fatalf("Failed to initialize SPIKE API: %v", err)
// }
// defer api.Close()
func New() (*API, *sdkErrors.SDKError) {
defaultEndpointSocket := spiffe.EndpointSocket()
ctx, cancel := context.WithTimeout(
context.Background(),
env.SPIFFESourceTimeoutVal(),
)
defer cancel()
source, _, err := spiffe.Source(ctx, defaultEndpointSocket)
if err != nil {
return nil, err
}
return &API{source: source}, nil
}
// NewWithSource initializes a new API instance with a pre-configured
// X509Source. This constructor is useful when you already have an X.509 source
// or need custom source configuration. The API instance will be configured to
// only communicate with SPIKE Nexus servers.
//
// Parameters:
// - source: A pre-configured X509Source that provides the client's identity
// certificates and trusted roots for server validation
//
// Returns:
// - *API: A configured API instance using the provided source
//
// Note: The API client created with this function is restricted to communicate
// only with SPIKE Nexus instances (using predicate.AllowNexus). If you need
// to connect to different servers, use New() with a custom predicate instead.
//
// Example usage:
//
// // Use with custom-configured source
// source, err := workloadapi.NewX509Source(ctx,
// workloadapi.WithClientOptions(...))
// if err != nil {
// log.Fatal("Failed to create X509Source")
// }
// api := NewWithSource(source)
// defer api.Close()
func NewWithSource(source *workloadapi.X509Source) *API {
return &API{
source: source,
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// BootstrapVerifyRequest for verifying SPIKE Nexus initialization.
type BootstrapVerifyRequest struct {
// Nonce used for encryption
Nonce []byte `json:"nonce"`
// Encrypted ciphertext to verify
Ciphertext []byte `json:"ciphertext"`
}
// BootstrapVerifyResponse contains the hash of the decrypted plaintext.
type BootstrapVerifyResponse struct {
// Hash of the decrypted plaintext
Hash string `json:"hash"`
// Error code if operation failed
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r BootstrapVerifyResponse) Success() BootstrapVerifyResponse {
r.Err = ""
return r
}
func (r BootstrapVerifyResponse) NotFound() BootstrapVerifyResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r BootstrapVerifyResponse) BadRequest() BootstrapVerifyResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r BootstrapVerifyResponse) Unauthorized() BootstrapVerifyResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r BootstrapVerifyResponse) Internal() BootstrapVerifyResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r BootstrapVerifyResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// CipherEncryptRequest for encrypting data
type CipherEncryptRequest struct {
// Plaintext data to encrypt
Plaintext []byte `json:"plaintext"`
// Optional: specify encryption algorithm/version
Algorithm string `json:"algorithm,omitempty"`
}
// CipherEncryptResponse contains encrypted data
type CipherEncryptResponse struct {
// Version byte for future compatibility
Version byte `json:"version"`
// Nonce used for encryption
Nonce []byte `json:"nonce"`
// Encrypted ciphertext
Ciphertext []byte `json:"ciphertext"`
// Error code if operation failed
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r CipherEncryptResponse) Success() CipherEncryptResponse {
r.Err = ""
return r
}
func (r CipherEncryptResponse) NotFound() CipherEncryptResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r CipherEncryptResponse) BadRequest() CipherEncryptResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r CipherEncryptResponse) Unauthorized() CipherEncryptResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r CipherEncryptResponse) Internal() CipherEncryptResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r CipherEncryptResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// CipherDecryptRequest for decrypting data
type CipherDecryptRequest struct {
// Version byte to determine decryption method
Version byte `json:"version"`
// Nonce used during encryption
Nonce []byte `json:"nonce"`
// Encrypted ciphertext to decrypt
Ciphertext []byte `json:"ciphertext"`
// Optional: specify decryption algorithm/version
Algorithm string `json:"algorithm,omitempty"`
}
// CipherDecryptResponse contains decrypted data
type CipherDecryptResponse struct {
// Decrypted plaintext data
Plaintext []byte `json:"plaintext"`
// Error code if operation failed
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r CipherDecryptResponse) Success() CipherDecryptResponse {
r.Err = ""
return r
}
func (r CipherDecryptResponse) NotFound() CipherDecryptResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r CipherDecryptResponse) BadRequest() CipherDecryptResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r CipherDecryptResponse) Unauthorized() CipherDecryptResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r CipherDecryptResponse) Internal() CipherDecryptResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r CipherDecryptResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/errors"
)
// FallbackResponse is a generic response for any error.
type FallbackResponse struct {
Err errors.ErrorCode `json:"err,omitempty"`
}
func (r FallbackResponse) ErrorCode() errors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// RestoreRequest for disaster recovery.
type RestoreRequest struct {
ID int `json:"id"`
Shard *[32]byte `json:"shard"`
}
// RestoreResponse for disaster recovery.
type RestoreResponse struct {
data.RestorationStatus
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r RestoreResponse) Success() RestoreResponse {
r.Err = ""
return r
}
func (r RestoreResponse) NotFound() RestoreResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r RestoreResponse) BadRequest() RestoreResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r RestoreResponse) Unauthorized() RestoreResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r RestoreResponse) Internal() RestoreResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r RestoreResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// RecoverRequest for disaster recovery.
type RecoverRequest struct {
}
// RecoverResponse for disaster recovery.
type RecoverResponse struct {
Shards map[int]*[32]byte `json:"shards"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r RecoverResponse) Success() RecoverResponse {
r.Err = ""
return r
}
func (r RecoverResponse) NotFound() RecoverResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r RecoverResponse) BadRequest() RecoverResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r RecoverResponse) Unauthorized() RecoverResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r RecoverResponse) Internal() RecoverResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r RecoverResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// PolicyPutRequest for policy creation.
type PolicyPutRequest struct {
Name string `json:"name"`
SPIFFEIDPattern string `json:"spiffeidPattern"`
PathPattern string `json:"pathPattern"`
Permissions []data.PolicyPermission `json:"permissions"`
}
// PolicyPutResponse for policy creation.
type PolicyPutResponse struct {
ID string `json:"id,omitempty"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r PolicyPutResponse) Success() PolicyPutResponse {
r.Err = ""
return r
}
func (r PolicyPutResponse) NotFound() PolicyPutResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r PolicyPutResponse) BadRequest() PolicyPutResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r PolicyPutResponse) Unauthorized() PolicyPutResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r PolicyPutResponse) Internal() PolicyPutResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r PolicyPutResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// PolicyReadRequest to read a policy.
type PolicyReadRequest struct {
ID string `json:"id"`
}
// PolicyReadResponse to read a policy.
type PolicyReadResponse struct {
data.Policy
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r PolicyReadResponse) Success() PolicyReadResponse {
r.Err = ""
return r
}
func (r PolicyReadResponse) NotFound() PolicyReadResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r PolicyReadResponse) BadRequest() PolicyReadResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r PolicyReadResponse) Unauthorized() PolicyReadResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r PolicyReadResponse) Internal() PolicyReadResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r PolicyReadResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// PolicyDeleteRequest to delete a policy.
type PolicyDeleteRequest struct {
ID string `json:"id"`
}
// PolicyDeleteResponse to delete a policy.
type PolicyDeleteResponse struct {
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r PolicyDeleteResponse) Success() PolicyDeleteResponse {
r.Err = ""
return r
}
func (r PolicyDeleteResponse) NotFound() PolicyDeleteResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r PolicyDeleteResponse) BadRequest() PolicyDeleteResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r PolicyDeleteResponse) Unauthorized() PolicyDeleteResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r PolicyDeleteResponse) Internal() PolicyDeleteResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r PolicyDeleteResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// PolicyListRequest to list policies.
type PolicyListRequest struct {
SPIFFEIDPattern string `json:"spiffeidPattern"`
PathPattern string `json:"pathPattern"`
}
// PolicyListResponse to list policies.
type PolicyListResponse struct {
Policies []data.Policy `json:"policies"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r PolicyListResponse) Success() PolicyListResponse {
r.Err = ""
return r
}
func (r PolicyListResponse) NotFound() PolicyListResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r PolicyListResponse) BadRequest() PolicyListResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r PolicyListResponse) Unauthorized() PolicyListResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r PolicyListResponse) Internal() PolicyListResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r PolicyListResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// PolicyAccessCheckRequest to validate policy access.
type PolicyAccessCheckRequest struct {
SPIFFEID string `json:"spiffeid"`
Path string `json:"path"`
Action string `json:"action"`
}
// PolicyAccessCheckResponse to validate policy access.
type PolicyAccessCheckResponse struct {
Allowed bool `json:"allowed"`
MatchingPolicies []string `json:"matchingPolicies"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r PolicyAccessCheckResponse) Success() PolicyAccessCheckResponse {
r.Err = ""
return r
}
func (r PolicyAccessCheckResponse) NotFound() PolicyAccessCheckResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r PolicyAccessCheckResponse) BadRequest() PolicyAccessCheckResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r PolicyAccessCheckResponse) Unauthorized() PolicyAccessCheckResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r PolicyAccessCheckResponse) Internal() PolicyAccessCheckResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r PolicyAccessCheckResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// SecretMetadataRequest for get secrets metadata
type SecretMetadataRequest struct {
Path string `json:"path"`
Version int `json:"version,omitempty"` // Optional specific version
}
// SecretMetadataResponse for secrets versions and metadata
type SecretMetadataResponse struct {
data.SecretMetadata
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretMetadataResponse) Success() SecretMetadataResponse {
r.Err = ""
return r
}
func (r SecretMetadataResponse) NotFound() SecretMetadataResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r SecretMetadataResponse) BadRequest() SecretMetadataResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretMetadataResponse) Unauthorized() SecretMetadataResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretMetadataResponse) Internal() SecretMetadataResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretMetadataResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// SecretPutRequest for creating/updating secrets
type SecretPutRequest struct {
Path string `json:"path"`
Values map[string]string `json:"values"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
// SecretPutResponse is after a successful secret write operation.
type SecretPutResponse struct {
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretPutResponse) Success() SecretPutResponse {
r.Err = ""
return r
}
func (r SecretPutResponse) NotFound() SecretPutResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r SecretPutResponse) BadRequest() SecretPutResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretPutResponse) Unauthorized() SecretPutResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretPutResponse) Internal() SecretPutResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretPutResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// SecretGetRequest is for getting secrets
type SecretGetRequest struct {
Path string `json:"path"`
Version int `json:"version,omitempty"` // Optional specific version
}
// SecretGetResponse is for getting secrets
type SecretGetResponse struct {
data.Secret
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretGetResponse) Success() SecretGetResponse {
r.Err = ""
return r
}
func (r SecretGetResponse) NotFound() SecretGetResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r SecretGetResponse) BadRequest() SecretGetResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretGetResponse) Unauthorized() SecretGetResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretGetResponse) Internal() SecretGetResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretGetResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// SecretDeleteRequest for soft-deleting secret versions
type SecretDeleteRequest struct {
Path string `json:"path"`
Versions []int `json:"versions"` // Empty means the latest version
}
// SecretDeleteResponse after soft-delete
type SecretDeleteResponse struct {
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretDeleteResponse) NotFound() SecretDeleteResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r SecretDeleteResponse) BadRequest() SecretDeleteResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretDeleteResponse) Unauthorized() SecretDeleteResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretDeleteResponse) Internal() SecretDeleteResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretDeleteResponse) Success() SecretDeleteResponse {
r.Err = ""
return r
}
func (r SecretDeleteResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// SecretUndeleteRequest for recovering soft-deleted versions
type SecretUndeleteRequest struct {
Path string `json:"path"`
Versions []int `json:"versions"`
}
// SecretUndeleteResponse after recovery
type SecretUndeleteResponse struct {
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretUndeleteResponse) Success() SecretUndeleteResponse {
r.Err = ""
return r
}
func (r SecretUndeleteResponse) NotFound() SecretUndeleteResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r SecretUndeleteResponse) BadRequest() SecretUndeleteResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretUndeleteResponse) Unauthorized() SecretUndeleteResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretUndeleteResponse) Internal() SecretUndeleteResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretUndeleteResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// SecretListRequest for listing secrets
type SecretListRequest struct {
}
// SecretListResponse for listing secrets
type SecretListResponse struct {
Keys []string `json:"keys"`
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r SecretListResponse) Success() SecretListResponse {
r.Err = ""
return r
}
func (r SecretListResponse) NotFound() SecretListResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r SecretListResponse) BadRequest() SecretListResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r SecretListResponse) Unauthorized() SecretListResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r SecretListResponse) Internal() SecretListResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r SecretListResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package reqres
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// ShardPutRequest represents a request to submit a shard contribution.
// KeeperId specifies the identifier of the keeper responsible for the shard.
// Shard represents the shard data being contributed to the system.
// Version optionally specifies the version of the shard being submitted.
type ShardPutRequest struct {
Shard *[32]byte `json:"shard"`
}
// ShardPutResponse represents the response structure for a shard
// contribution.
type ShardPutResponse struct {
Err sdkErrors.ErrorCode `json:"err,omitempty"`
}
func (r ShardPutResponse) Success() ShardPutResponse {
r.Err = ""
return r
}
func (r ShardPutResponse) NotFound() ShardPutResponse {
log.FatalErr("NotFound", *sdkErrors.ErrAPIResponseCodeInvalid)
return r
}
func (r ShardPutResponse) BadRequest() ShardPutResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r ShardPutResponse) Unauthorized() ShardPutResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r ShardPutResponse) Internal() ShardPutResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r ShardPutResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// ShardGetRequest represents a request to get a Shamir shard.
type ShardGetRequest struct {
}
// ShardGetResponse represents the response that returns a Shamir shard.
// The struct includes the shard identifier and an associated error code.
type ShardGetResponse struct {
Shard *[32]byte `json:"shard"`
Err sdkErrors.ErrorCode
}
func (r ShardGetResponse) Success() ShardGetResponse {
r.Err = ""
return r
}
func (r ShardGetResponse) NotFound() ShardGetResponse {
r.Err = sdkErrors.ErrAPINotFound.Code
return r
}
func (r ShardGetResponse) BadRequest() ShardGetResponse {
r.Err = sdkErrors.ErrAPIBadRequest.Code
return r
}
func (r ShardGetResponse) Unauthorized() ShardGetResponse {
r.Err = sdkErrors.ErrAccessUnauthorized.Code
return r
}
func (r ShardGetResponse) Internal() ShardGetResponse {
r.Err = sdkErrors.ErrAPIInternal.Code
return r
}
func (r ShardGetResponse) ErrorCode() sdkErrors.ErrorCode {
return r.Err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package config
import (
"os"
"path/filepath"
)
const tmpRootDir = "/tmp"
const dataRootDir = "/data"
const dataDir = ".spike"
// NexusDataFolder returns the path to the directory where Nexus stores
// its encrypted backup for its secrets and other data.
func NexusDataFolder() string {
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir = tmpRootDir
}
sd := filepath.Join(homeDir, dataDir)
sdr := filepath.Join(sd, dataRootDir)
// Create the directory if it doesn't exist
// 0700 because we want to restrict access to the directory
// but allow the user to create db files in it.
err = os.MkdirAll(sdr, 0700)
if err != nil {
panic(err)
}
// The data dir is not configurable for security reasons.
return sdr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// CreatePolicy creates a new policy in the system using the provided SPIFFE
// X.509 source and policy details. It establishes a mutual TLS connection to
// SPIKE Nexus using the X.509 source and sends a policy creation request.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - name: The name of the policy to be created
// - SPIFFEIDPattern: The SPIFFE ID pattern that this policy will apply to
// - pathPattern: The path pattern that this policy will match against
// - permissions: A slice of PolicyPermission defining the access rights
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPIPostFailed: if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error (e.g.,
// ErrAccessUnauthorized, ErrAPIBadRequest, etc.)
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// permissions := []data.PolicyPermission{
// {
// Action: "read",
// Resource: "documents/*",
// },
// }
//
// err = CreatePolicy(
// source,
// "doc-reader",
// "spiffe://example.org/service/*",
// "/api/documents/*",
// permissions,
// )
// if err != nil {
// log.Printf("Failed to create policy: %v", err)
// }
func CreatePolicy(source *workloadapi.X509Source,
name string, SPIFFEIDPattern string, pathPattern string,
permissions []data.PolicyPermission,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.PolicyPutRequest{
Name: name,
SPIFFEIDPattern: SPIFFEIDPattern,
PathPattern: pathPattern,
Permissions: permissions,
}
var mr []byte
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
_, postErr := net.PostAndUnmarshal[reqres.PolicyPutResponse](
source, url.PolicyCreate(), mr)
return postErr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// DeletePolicy removes an existing policy from the system using its ID.
// It establishes a mutual TLS connection to SPIKE Nexus using the X.509 source
// and sends a policy deletion request.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - id: The unique identifier of the policy to be deleted
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPIPostFailed: if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error (e.g.,
// ErrAccessUnauthorized, ErrAPINotFound, ErrAPIBadRequest, etc.)
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// err = DeletePolicy(source, "policy-123")
// if err != nil {
// log.Printf("Failed to delete policy: %v", err)
// }
func DeletePolicy(
source *workloadapi.X509Source,
id string,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.PolicyDeleteRequest{ID: id}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
_, postErr := net.PostAndUnmarshal[reqres.PolicyDeleteResponse](
source, url.PolicyDelete(), mr)
return postErr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// GetPolicy retrieves a policy from the system using its ID.
// It establishes a mutual TLS connection to SPIKE Nexus using the X.509 source
// and sends a policy retrieval request.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - id: The unique identifier of the policy to retrieve
//
// Returns:
// - *data.Policy: The policy if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the policy is not found
// - ErrAPIPostFailed: if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error (e.g.,
// ErrAccessUnauthorized, ErrAPIBadRequest, etc.)
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// policy, err := GetPolicy(source, "policy-123")
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// log.Printf("Policy not found")
// return
// }
// log.Printf("Error retrieving policy: %v", err)
// return
// }
//
// log.Printf("Found policy: %+v", policy)
func GetPolicy(
source *workloadapi.X509Source, id string,
) (*data.Policy, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.PolicyReadRequest{ID: id}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.PolicyReadResponse](
source, url.PolicyGet(), mr)
if postErr != nil {
return nil, postErr
}
return &res.Policy, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package acl
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// ListPolicies retrieves policies from the system, optionally filtering by
// SPIFFE ID and path patterns. It establishes a mutual TLS connection to
// SPIKE Nexus using the X.509 source and sends a policy list request.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - SPIFFEIDPattern: The SPIFFE ID pattern to filter policies. An empty
// string matches all SPIFFE IDs.
// - pathPattern: The path pattern to filter policies. An empty string
// matches all paths.
//
// Returns:
// - (*[]data.Policy, nil) containing all matching policies if successful
// - (nil, nil) if no policies are found
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPIPostFailed: if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error (e.g.,
// ErrAccessUnauthorized, ErrAPIBadRequest, etc.)
//
// Note: The returned slice pointer should be dereferenced before use:
//
// policies := *result
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// // List all policies
// result, err := ListPolicies(source, "", "")
// if err != nil {
// log.Printf("Error listing policies: %v", err)
// return
// }
// if result == nil {
// log.Printf("No policies found")
// return
// }
//
// policies := *result
// for _, policy := range policies {
// log.Printf("Found policy: %+v", policy)
// }
func ListPolicies(
source *workloadapi.X509Source,
SPIFFEIDPattern string, pathPattern string,
) (*[]data.Policy, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.PolicyListRequest{
SPIFFEIDPattern: SPIFFEIDPattern,
PathPattern: pathPattern,
}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.PolicyListResponse](
source, url.PolicyList(), mr)
if postErr != nil {
if postErr.Is(sdkErrors.ErrAPINotFound) {
return &([]data.Policy{}), nil
}
return nil, postErr
}
return &res.Policies, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/cloudflare/circl/secretsharing"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
"github.com/spiffe/spike-sdk-go/config/env"
"github.com/spiffe/spike-sdk-go/crypto"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/net"
"github.com/spiffe/spike-sdk-go/security/mem"
)
// Contribute sends a secret share contribution to a SPIKE Keeper during the
// bootstrap process. It establishes a mutual TLS connection to the specified
// Keeper and transmits the keeper's share of the secret.
//
// The function marshals the share value, validates its length, and sends it
// securely to the Keeper. After sending, the contribution is zeroed out in
// memory for security.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Keeper
// - keeperShare: The secret share to contribute to the Keeper
// - keeperID: The unique identifier of the target Keeper
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from net.Post(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady,
// ErrNetPeerConnection)
//
// Note: The function will fatally crash (via log.FatalErr) for unrecoverable
// errors such as marshal failures (ErrDataMarshalFailure) or invalid
// contribution length (ErrCryptoInvalidEncryptionKeyLength).
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// err = Contribute(source, keeperShare, "keeper-1")
// if err != nil {
// log.Printf("Failed to contribute share: %v", err)
// }
func Contribute(
source *workloadapi.X509Source,
keeperShare secretsharing.Share,
keeperID string,
) *sdkErrors.SDKError {
const fName = "Contribute"
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
contribution, err := keeperShare.Value.MarshalBinary()
if err != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(err)
failErr.Msg = "failed to marshal share"
log.FatalErr(fName, *failErr)
}
if len(contribution) != crypto.AES256KeySize {
failErr := sdkErrors.ErrCryptoInvalidEncryptionKeyLength
failErr.Msg = fmt.Sprintf(
"invalid contribution length: expected %d, got %d",
crypto.AES256KeySize, len(contribution),
)
log.FatalErr(fName, *failErr)
}
scr := reqres.ShardPutRequest{}
shard := new([crypto.AES256KeySize]byte)
copy(shard[:], contribution)
// Security: Zero out contribution as soon as we don't need it.
mem.ClearBytes(contribution)
scr.Shard = shard
client := net.CreateMTLSClientForKeeper(source)
for kid, keeperAPIRoot := range env.KeepersVal() {
if kid != keeperID {
// These are not the keepers we are looking for...
continue
}
md, marshalErr := json.Marshal(scr)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "failed to marshal request"
log.FatalErr(fName, *failErr)
}
u := url.KeeperBootstrapContributeEndpoint(keeperAPIRoot)
_, sdkErr := net.Post(client, u, md)
if sdkErr != nil {
return sdkErr
}
}
return nil
}
// Verify performs bootstrap verification with SPIKE Nexus by sending encrypted
// random text and validating that Nexus can decrypt it correctly. This ensures
// that the bootstrap process completed successfully and Nexus has the correct
// master key.
//
// The function sends the nonce and ciphertext to Nexus, receives back a hash,
// and compares it against the expected hash of the original random text. A
// match confirms successful bootstrap.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - randomText: The original random text that was encrypted
// - nonce: The nonce used during encryption
// - ciphertext: The encrypted random text
//
// Returns:
// - *sdkErrors.SDKError: nil on success (hash matches), or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from net.Post(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady, ErrNetPeerConnection)
//
// Note: The function will fatally crash (via log.FatalErr) for unrecoverable
// errors such as marshal failures (ErrDataMarshalFailure), response parsing
// failures (ErrDataUnmarshalFailure), or hash verification failures
// (ErrCryptoCipherVerificationFailed). These indicate potential security
// issues and the application should not continue.
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// err = Verify(source, randomText, nonce, ciphertext)
// if err != nil {
// log.Printf("Bootstrap verification failed: %v", err)
// }
func Verify(
source *workloadapi.X509Source,
randomText string,
nonce, ciphertext []byte,
) *sdkErrors.SDKError {
const fName = "Verify"
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
client := net.CreateMTLSClientForNexus(source)
request := reqres.BootstrapVerifyRequest{
Nonce: nonce,
Ciphertext: ciphertext,
}
md, marshalErr := json.Marshal(request)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "failed to marshal verification request"
log.FatalErr(fName, *failErr)
}
// Send the verification request to SPIKE Nexus
nexusAPIRoot := env.NexusAPIRootVal()
verifyURL := url.NexusBootstrapVerifyEndpoint(nexusAPIRoot)
log.Info(
fName,
"message", "sending verification request to SPIKE Nexus",
"url", verifyURL,
)
responseBody, err := net.Post(client, verifyURL, md)
if err != nil {
return err
}
// Parse the response
var verifyResponse struct {
Hash string `json:"hash"`
Err string `json:"err"`
}
if unmarshalErr := json.Unmarshal(
responseBody, &verifyResponse,
); unmarshalErr != nil {
failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr)
failErr.Msg = "failed to parse verification response"
// If SPIKE Keeper is sending gibberish, it may be a malicious actor.
// Fatally crash here to prevent a possible compromise.
log.FatalErr(fName, *failErr)
}
// Compute the expected hash
expectedHash := sha256.Sum256([]byte(randomText))
expectedHashHex := hex.EncodeToString(expectedHash[:])
// Verify the hash matches
if verifyResponse.Hash != expectedHashHex {
failErr := sdkErrors.ErrCryptoCipherVerificationFailed
failErr.Msg = "verification failed: hash mismatch"
log.FatalErr(fName, *failErr)
}
return nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package cipher
import (
"encoding/json"
"io"
"net/http"
"github.com/spiffe/go-spiffe/v2/workloadapi"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/net"
)
// Cipher encapsulates cipher operations with configurable HTTP client
// dependencies. This struct-based approach enables clean dependency injection
// for testing without relying on the global mutable state.
//
// The zero value is not usable; instances should be created using NewCipher().
type Cipher struct {
// createMTLSHTTPClientFromSource creates an mTLS HTTP client
// from an X509Source
createMTLSHTTPClientFromSource func(*workloadapi.X509Source) *http.Client
// httpPost performs a POST request and returns the response body
httpPost func(*http.Client, string, []byte) ([]byte, *sdkErrors.SDKError)
// streamPost performs a streaming POST request with binary data
// (always uses application/octet-stream content type)
streamPost func(
*http.Client, string, io.Reader,
) (io.ReadCloser, *sdkErrors.SDKError)
}
// NewCipher creates a new Cipher instance with default production dependencies.
// The returned Cipher is ready to use for encryption and decryption operations.
//
// For testing, create a Cipher with custom dependencies by directly
// constructing the struct with test doubles.
//
// Example:
//
// cipher := NewCipher()
// plaintext, err := cipher.Encrypt(source, data, "AES-GCM")
func NewCipher() *Cipher {
return &Cipher{
createMTLSHTTPClientFromSource: net.CreateMTLSClientForNexus,
httpPost: net.Post,
streamPost: net.StreamPost,
}
}
// streamOperation performs a streaming encryption or decryption operation.
// This is a common helper that removes duplication between EncryptStream
// and DecryptStream.
//
// Parameters:
// - source: X509Source for establishing mTLS connection
// - r: io.Reader containing the data to process
// - urlPath: The API endpoint URL
// - fName: Function name for logging purposes
//
// Returns:
// - []byte: The processed data if successful
// - *sdkErrors.SDKError: Error if the operation fails
func (c *Cipher) streamOperation(
source *workloadapi.X509Source,
r io.Reader,
urlPath string,
fName string,
) ([]byte, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
client := c.createMTLSHTTPClientFromSource(source)
rc, err := c.streamPost(client, urlPath, r)
if err != nil {
return nil, err
}
defer func(rc io.ReadCloser) {
if rc == nil {
return
}
closeErr := rc.Close()
if closeErr != nil {
failErr := sdkErrors.ErrFSStreamCloseFailed.Wrap(closeErr)
failErr.Msg = "failed to close response body"
log.WarnErr(fName, *failErr)
}
}(rc)
b, readErr := io.ReadAll(rc)
if readErr != nil {
failErr := sdkErrors.ErrNetReadingResponseBody.Wrap(readErr)
failErr.Msg = "failed to read response body"
return nil, failErr
}
return b, nil
}
// jsonOperation performs a JSON-based operation with generic request/response
// handling. This helper removes duplication between Encrypt and Decrypt
// operations.
//
// Parameters:
// - source: X509Source for establishing mTLS connection
// - request: The request payload (will be marshaled to JSON)
// - urlPath: The API endpoint URL
// - response: Pointer to response struct that implements ResponseWithError
//
// Returns:
// - *sdkErrors.SDKError: Error if the operation fails, nil on success
func (c *Cipher) jsonOperation(
source *workloadapi.X509Source, request any, urlPath string, response any,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
client := c.createMTLSHTTPClientFromSource(source)
mr, marshalErr := json.Marshal(request)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
body, err := c.httpPost(client, urlPath, mr)
if err != nil {
return err
}
if unmarshalErr := json.Unmarshal(body, response); unmarshalErr != nil {
failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr)
failErr.Msg = "problem parsing response body"
return failErr
}
// Type assertion to check error code
// Doing this with generics would be tricky in Go's current type system.
if respWithErr, ok := response.(net.ResponseWithError); ok {
if errCode := respWithErr.ErrorCode(); errCode != "" {
return sdkErrors.FromCode(errCode)
}
}
return nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package cipher
import (
"io"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// DecryptStream decrypts data from a reader using streaming mode using the
// default Cipher instance.
// It sends the reader content as the request body and returns the decrypted
// plaintext bytes. The data is treated as binary (application/octet-stream)
// as decryption operates on raw encrypted bytes.
//
// This is a convenience function that uses the default Cipher instance.
// For testing or custom configuration, create a Cipher instance directly.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - r: io.Reader containing the encrypted data
//
// Returns:
// - ([]byte, nil) containing the decrypted plaintext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// reader := bytes.NewReader(encryptedData)
// plaintext, err := DecryptStream(source, reader)
// if err != nil {
// log.Printf("Decryption failed: %v", err)
// }
func DecryptStream(
source *workloadapi.X509Source, r io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return NewCipher().DecryptStream(source, r)
}
// DecryptStream decrypts data from a reader using streaming mode.
// It sends the reader content as the request body and returns the decrypted
// plaintext bytes. The data is treated as binary (application/octet-stream)
// as decryption operates on raw encrypted bytes.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - r: io.Reader containing the encrypted data
//
// Returns:
// - ([]byte, nil) containing the decrypted plaintext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// cipher := NewCipher()
// reader := bytes.NewReader(encryptedData)
// plaintext, err := cipher.DecryptStream(source, reader)
// if err != nil {
// log.Printf("Decryption failed: %v", err)
// }
func (c *Cipher) DecryptStream(
source *workloadapi.X509Source, r io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return c.streamOperation(source, r, url.CipherDecrypt(), "DecryptStream")
}
// Decrypt decrypts data with structured parameters using
// the default Cipher instance.
// It sends version, nonce, ciphertext, and algorithm and returns
// decrypted plaintext bytes.
//
// This is a convenience function that uses the default Cipher instance.
// For testing or custom configuration, create a Cipher instance directly.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - version: The cipher version used during encryption
// - nonce: The nonce bytes used during encryption
// - ciphertext: The encrypted data to decrypt
// - algorithm: The encryption algorithm used (e.g., "AES-GCM")
//
// Returns:
// - ([]byte, nil) containing the decrypted plaintext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady,
// ErrNetPeerConnection)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// plaintext, err := Decrypt(source, 1, nonce, ciphertext, "AES-GCM")
// if err != nil {
// log.Printf("Decryption failed: %v", err)
// }
func Decrypt(
source *workloadapi.X509Source,
version byte, nonce, ciphertext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
return NewCipher().Decrypt(source, version, nonce, ciphertext, algorithm)
}
// Decrypt decrypts data with structured parameters.
// It sends version, nonce, ciphertext, and algorithm and returns
// decrypted plaintext bytes.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - version: The cipher version used during encryption
// - nonce: The nonce bytes used during encryption
// - ciphertext: The encrypted data to decrypt
// - algorithm: The encryption algorithm used (e.g., "AES-GCM")
//
// Returns:
// - ([]byte, nil) containing the decrypted plaintext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady,
// ErrNetPeerConnection)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// cipher := NewCipher()
// plaintext, err := cipher.Decrypt(source, 1, nonce, ciphertext, "AES-GCM")
// if err != nil {
// log.Printf("Decryption failed: %v", err)
// }
func (c *Cipher) Decrypt(
source *workloadapi.X509Source,
version byte, nonce, ciphertext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
payload := reqres.CipherDecryptRequest{
Version: version,
Nonce: nonce,
Ciphertext: ciphertext,
Algorithm: algorithm,
}
var res reqres.CipherDecryptResponse
if err := c.jsonOperation(
source, payload, url.CipherDecrypt(), &res,
); err != nil {
return nil, err
}
return res.Plaintext, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package cipher
import (
"io"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// EncryptStream encrypts data from a reader using streaming mode using the
// default Cipher instance.
// It sends the reader content as the request body and returns the encrypted
// ciphertext bytes. The data is treated as binary (application/octet-stream)
// as encryption operates on raw bytes.
//
// This is a convenience function that uses the default Cipher instance.
// For testing or custom configuration, create a Cipher instance directly.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - r: io.Reader containing the data to encrypt
//
// Returns:
// - ([]byte, nil) containing the encrypted ciphertext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// reader := bytes.NewReader([]byte("sensitive data"))
// ciphertext, err := EncryptStream(source, reader)
// if err != nil {
// log.Printf("Encryption failed: %v", err)
// }
func EncryptStream(
source *workloadapi.X509Source, r io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return NewCipher().EncryptStream(source, r)
}
// EncryptStream encrypts data from a reader using streaming mode.
// It sends the reader content as the request body and returns the encrypted
// ciphertext bytes. The data is treated as binary (application/octet-stream)
// as encryption operates on raw bytes.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - r: io.Reader containing the data to encrypt
//
// Returns:
// - ([]byte, nil) containing the encrypted ciphertext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - Errors from streamPost(): if the streaming request fails
// - ErrNetReadingResponseBody: if reading the response fails
//
// Example:
//
// cipher := NewCipher()
// reader := bytes.NewReader([]byte("sensitive data"))
// ciphertext, err := cipher.EncryptStream(source, reader)
// if err != nil {
// log.Printf("Encryption failed: %v", err)
// }
func (c *Cipher) EncryptStream(
source *workloadapi.X509Source, r io.Reader,
) ([]byte, *sdkErrors.SDKError) {
return c.streamOperation(source, r, url.CipherEncrypt(), "EncryptStream")
}
// Encrypt encrypts data with structured parameters using
// the default Cipher instance.
// It sends plaintext and algorithm and returns encrypted ciphertext
// bytes.
//
// This is a convenience function that uses the default Cipher instance.
// For testing or custom configuration, create a Cipher instance directly.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - plaintext: The data to encrypt
// - algorithm: The encryption algorithm to use (e.g., "AES-GCM")
//
// Returns:
// - ([]byte, nil) containing the encrypted ciphertext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady,
// ErrNetPeerConnection)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// source, err := workloadapi.NewX509Source(ctx)
// if err != nil {
// log.Fatal(err)
// }
// defer source.Close()
//
// data := []byte("secret message")
// ciphertext, err := Encrypt(source, data, "AES-GCM")
// if err != nil {
// log.Printf("Encryption failed: %v", err)
// }
func Encrypt(
source *workloadapi.X509Source, plaintext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
return NewCipher().Encrypt(source, plaintext, algorithm)
}
// Encrypt encrypts data with structured parameters.
// It sends plaintext and algorithm and returns encrypted ciphertext
// bytes.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - plaintext: The data to encrypt
// - algorithm: The encryption algorithm to use (e.g., "AES-GCM")
//
// Returns:
// - ([]byte, nil) containing the encrypted ciphertext if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from httpPost(): if the HTTP request fails (e.g., ErrAPINotFound,
// ErrAccessUnauthorized, ErrAPIBadRequest, ErrStateNotReady,
// ErrNetPeerConnection)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// cipher := NewCipher()
// data := []byte("secret message")
// ciphertext, err := cipher.Encrypt(source, data, "AES-GCM")
// if err != nil {
// log.Printf("Encryption failed: %v", err)
// }
func (c *Cipher) Encrypt(
source *workloadapi.X509Source, plaintext []byte, algorithm string,
) ([]byte, *sdkErrors.SDKError) {
payload := reqres.CipherEncryptRequest{
Plaintext: plaintext,
Algorithm: algorithm,
}
var res reqres.CipherEncryptResponse
if err := c.jsonOperation(
source, payload, url.CipherEncrypt(), &res,
); err != nil {
return nil, err
}
return res.Ciphertext, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/net"
"github.com/spiffe/spike-sdk-go/spiffeid"
)
// Recover makes a request to initiate recovery of secrets, returning the
// recovery shards.
//
// SVID Acquisition Error Handling:
//
// This function attempts to acquire an X.509 SVID from the SPIFFE Workload API
// via Unix domain socket. While UDS connections are generally more reliable than
// network sockets, SVID acquisition can fail in both fatal and transient ways:
//
// Fatal failures (indicate misconfiguration):
// - Socket file doesn't exist (SPIRE agent never started)
// - Permission denied (deployment/configuration error)
// - Wrong socket path (configuration error)
//
// Transient failures (may succeed on retry):
// - SPIRE agent restarting (brief unavailability, recovers in seconds)
// - SVID not yet provisioned (startup race condition after attestation)
// - File descriptor exhaustion (resource pressure may clear)
// - SVID rotation failure (temporary SPIRE server issue)
// - Workload API connection lost after source creation (agent crash/restart)
// - If the SPIFFE provider is SPIRE the workload might not be registered;
// or the registration entry might not be propagated through the system yet,
// - The workload attestation server, kubelet, or even kubeapi-server might
// be overloaded and can't answer the requests from the agent, or it may
// even be hard to read data from the /proc/ or the cgroup filesystem.
//
// Since recovery is often performed during emergency procedures when
// infrastructure may be unstable, this function returns errors rather than
// crashing to allow retry logic. Callers can implement exponential backoff
// or other retry strategies for transient failures.
//
// Parameters:
// - source: X509Source used for mTLS client authentication
//
// Returns:
// - map[int]*[32]byte: Map of shard indices to shard byte arrays if successful
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrSPIFFEFailedToExtractX509SVID: if SVID acquisition fails (may be
// transient - see above for retry guidance)
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Security Note: The function will fatally crash (via log.FatalErr) if the
// caller is not SPIKE Pilot. This is a programming error, not a runtime
// condition, as recovery operations must only be performed by Pilot roles.
//
// Example:
//
// shards, err := Recover(x509Source)
// if err != nil {
// // SVID acquisition failures may be transient - consider retry logic
// return nil, err
// }
func Recover(source *workloadapi.X509Source) (
map[int]*[32]byte, *sdkErrors.SDKError,
) {
const fName = "recover"
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
svid, err := source.GetX509SVID()
if err != nil {
failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err)
failErr.Msg = "could not acquire SVID"
return nil, failErr
}
if svid == nil {
failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID
failErr.Msg = "no X509SVID in source"
return nil, failErr
}
selfSPIFFEID := svid.ID.String()
// Security: Recovery and Restoration can ONLY be done via SPIKE Pilot.
if !spiffeid.IsPilot(selfSPIFFEID) {
failErr := sdkErrors.ErrAccessUnauthorized
failErr.Msg = "recovery can only be performed from SPIKE Pilot"
log.FatalErr(fName, *failErr)
}
r := reqres.RecoverRequest{}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "failed to marshal recover request"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.RecoverResponse](
source, url.Recover(), mr)
if postErr != nil {
return nil, postErr
}
result := make(map[int]*[32]byte)
for i, shard := range res.Shards {
result[i] = shard
}
return result, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package operator
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/net"
"github.com/spiffe/spike-sdk-go/spiffeid"
)
// Restore submits a recovery shard to continue the restoration process.
//
// SVID Acquisition Error Handling:
//
// This function attempts to acquire an X.509 SVID from the SPIFFE Workload API
// via Unix domain socket. While UDS connections are generally more reliable than
// network sockets, SVID acquisition can fail in both fatal and transient ways:
//
// Fatal failures (indicate misconfiguration):
// - Socket file doesn't exist (SPIRE agent never started)
// - Permission denied (deployment/configuration error)
// - Wrong socket path (configuration error)
//
// Transient failures (may succeed on retry):
// - SPIRE agent restarting (brief unavailability, recovers in seconds)
// - SVID not yet provisioned (startup race condition after attestation)
// - File descriptor exhaustion (resource pressure may clear)
// - SVID rotation failure (temporary SPIRE server issue)
// - Workload API connection lost after source creation (agent crash/restart)
//
// Since restoration is often performed during emergency procedures when
// infrastructure may be unstable, this function returns errors rather than
// crashing to allow retry logic. Callers can implement exponential backoff
// or other retry strategies for transient failures.
//
// Parameters:
// - source *workloadapi.X509Source: X509Source used for mTLS client
// authentication
// - shardIndex int: Index of the recovery shard
// - shardValue *[32]byte: Pointer to a 32-byte array containing the recovery
// shard
//
// Returns:
// - *data.RestorationStatus: Status containing shards collected, remaining,
// and restoration state if successful
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrSPIFFEFailedToExtractX509SVID: if SVID acquisition fails (may be
// transient - see above for retry guidance)
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Security Note: The function will fatally crash (via log.FatalErr) if the
// caller is not SPIKE Pilot. This is a programming error, not a runtime
// condition, as restoration operations must only be performed by Pilot roles.
//
// Example:
//
// status, err := Restore(x509Source, shardIndex, shardValue)
// if err != nil {
// // SVID acquisition failures may be transient - consider retry logic
// return nil, err
// }
func Restore(
source *workloadapi.X509Source, shardIndex int, shardValue *[32]byte,
) (*data.RestorationStatus, *sdkErrors.SDKError) {
const fName = "restore"
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.RestoreRequest{ID: shardIndex, Shard: shardValue}
svid, err := source.GetX509SVID()
if err != nil {
failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err)
failErr.Msg = "could not acquire SVID"
return nil, failErr
}
if svid == nil {
failErr := sdkErrors.ErrSPIFFEFailedToExtractX509SVID
failErr.Msg = "no X509SVID in source"
return nil, failErr
}
selfSPIFFEID := svid.ID.String()
// Security: Recovery and Restoration can ONLY be done via SPIKE Pilot.
if !spiffeid.IsPilot(selfSPIFFEID) {
failErr := sdkErrors.ErrAccessUnauthorized
failErr.Msg = "restoration can only be performed from SPIKE Pilot"
log.FatalErr(fName, *failErr)
}
mr, marshalErr := json.Marshal(r)
// Security: Zero out r.Shard as soon as we're done with it
for i := range r.Shard {
r.Shard[i] = 0
}
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "failed to marshal restore request"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.RestoreResponse](
source, url.Restore(), mr)
// Security: Zero out mr after the POST request is complete
for i := range mr {
mr[i] = 0
}
if postErr != nil {
return nil, postErr
}
return &data.RestorationStatus{
ShardsCollected: res.ShardsCollected,
ShardsRemaining: res.ShardsRemaining,
Restored: res.Restored,
}, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// Delete deletes specified versions of a secret at the given path.
//
// It converts string version numbers to integers, constructs a delete request,
// and sends it to the secrets API endpoint. If no versions are specified or
// the conversion fails, no versions will be deleted.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - path: Path to the secret to delete
// - versions: Integer array of version numbers to delete
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := Delete(x509Source, "secret/path", []int{1, 2})
func Delete(
source *workloadapi.X509Source,
path string, versions []int,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.SecretDeleteRequest{Path: path, Versions: versions}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
_, postErr := net.PostAndUnmarshal[reqres.SecretDeleteResponse](
source, url.SecretDelete(), mr)
return postErr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// Get retrieves a specific version of a secret at the given path.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - path: Path to the secret to retrieve
// - version: Version number of the secret to retrieve
//
// Returns:
// - *data.Secret: Secret data if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the secret is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// secret, err := Get(x509Source, "secret/path", 1)
func Get(
source *workloadapi.X509Source,
path string, version int,
) (*data.Secret, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.SecretGetRequest{Path: path, Version: version}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.SecretGetResponse](
source, url.SecretGet(), mr)
if postErr != nil {
return nil, postErr
}
return &data.Secret{Data: res.Data}, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// ListKeys retrieves all secret keys.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
//
// Returns:
// - *[]string: Array of secret keys if found, empty array if no secrets exist
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Note: Returns (*[]string{}, nil) if no secrets are found (ErrAPINotFound)
//
// Example:
//
// keys, err := ListKeys(x509Source)
func ListKeys(
source *workloadapi.X509Source,
) (*[]string, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.SecretListRequest{}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.SecretListResponse](
source, url.SecretList(), mr)
if postErr != nil {
if postErr.Is(sdkErrors.ErrAPINotFound) {
return &[]string{}, nil
}
return nil, postErr
}
return &res.Keys, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// GetMetadata retrieves a specific version of a secret metadata at the
// given path.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - path: Path to the secret to retrieve
// - version: Version number of the secret to retrieve
//
// Returns:
// - *data.SecretMetadata: Secret metadata if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the secret metadata is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// metadata, err := GetMetadata(x509Source, "secret/path", 1)
func GetMetadata(
source *workloadapi.X509Source, path string, version int,
) (*data.SecretMetadata, *sdkErrors.SDKError) {
if source == nil {
return nil, sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.SecretMetadataRequest{Path: path, Version: version}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return nil, failErr
}
res, postErr := net.PostAndUnmarshal[reqres.SecretMetadataResponse](
source, url.SecretMetadataGet(), mr)
if postErr != nil {
return nil, postErr
}
return &data.SecretMetadata{
Versions: res.Versions,
Metadata: res.Metadata,
}, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// Put creates or updates a secret at the specified path with the given
// values.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - path: Path where the secret should be stored
// - values: Map of key-value pairs representing the secret data
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := Put(x509Source, "secret/path",
// map[string]string{"key": "value"})
func Put(
source *workloadapi.X509Source,
path string, values map[string]string,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
r := reqres.SecretPutRequest{Path: path, Values: values}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
_, postErr := net.PostAndUnmarshal[reqres.SecretPutResponse](
source, url.SecretPut(), mr)
return postErr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package secret
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/spiffe/spike-sdk-go/api/entity/v1/reqres"
"github.com/spiffe/spike-sdk-go/api/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/net"
)
// Undelete restores previously deleted versions of a secret at the
// specified path.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - path: Path to the secret to restore
// - versions: Integer array of version numbers to restore. Empty array
// attempts no restoration
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := Undelete(x509Source, "secret/path", []int{1, 2})
func Undelete(source *workloadapi.X509Source,
path string, versions []int,
) *sdkErrors.SDKError {
if source == nil {
return sdkErrors.ErrSPIFFENilX509Source
}
var vv []int
if len(versions) == 0 {
vv = []int{}
} else {
vv = versions
}
r := reqres.SecretUndeleteRequest{Path: path, Versions: vv}
mr, marshalErr := json.Marshal(r)
if marshalErr != nil {
failErr := sdkErrors.ErrDataMarshalFailure.Wrap(marshalErr)
failErr.Msg = "problem generating the payload"
return failErr
}
_, postErr := net.PostAndUnmarshal[reqres.SecretUndeleteResponse](
source, url.SecretUndelete(), mr)
return postErr
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/internal/impl/acl"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// CreatePolicy creates a new policy in the system.
//
// It establishes a mutual TLS connection using the X.509 source and sends a
// policy creation request to SPIKE Nexus.
//
// Parameters:
// - name: The name of the policy to be created
// - SPIFFEIDPattern: The SPIFFE ID pattern that this policy will apply to
// - pathPattern: The path pattern that this policy will match against
// - permissions: A slice of PolicyPermission defining the access rights
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// permissions := []data.PolicyPermission{
// {Action: "read", Resource: "documents/*"},
// }
// err := api.CreatePolicy(
// "doc-reader",
// "spiffe://example.org/service/*",
// "/api/documents/*",
// permissions,
// )
// if err != nil {
// log.Printf("Failed to create policy: %v", err)
// }
func (a *API) CreatePolicy(
name string, SPIFFEIDPattern string, pathPattern string,
permissions []data.PolicyPermission,
) *sdkErrors.SDKError {
return acl.CreatePolicy(a.source,
name, SPIFFEIDPattern, pathPattern, permissions)
}
// DeletePolicy removes an existing policy from the system using its unique ID.
//
// Parameters:
// - id: The unique identifier of the policy to be deleted
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := api.DeletePolicy("policy-123")
// if err != nil {
// log.Printf("Failed to delete policy: %v", err)
// }
func (a *API) DeletePolicy(id string) *sdkErrors.SDKError {
return acl.DeletePolicy(a.source, id)
}
// GetPolicy retrieves a policy from the system using its unique ID.
//
// Parameters:
// - id: The unique identifier of the policy to retrieve
//
// Returns:
// - *data.Policy: The policy if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the policy is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// policy, err := api.GetPolicy("policy-123")
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// log.Printf("Policy not found")
// return
// }
// log.Printf("Error retrieving policy: %v", err)
// return
// }
// log.Printf("Found policy: %+v", policy)
func (a *API) GetPolicy(id string) (*data.Policy, *sdkErrors.SDKError) {
return acl.GetPolicy(a.source, id)
}
// ListPolicies retrieves policies from the system, optionally filtering by
// SPIFFE ID and path patterns.
//
// Parameters:
// - SPIFFEIDPattern: The SPIFFE ID pattern to filter policies (empty string
// matches all SPIFFE IDs)
// - pathPattern: The path pattern to filter policies (empty string matches
// all paths)
//
// Returns:
// - *[]data.Policy: Array of matching policies, empty array if none found,
// nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Note: Returns (&[]data.Policy{}, nil) if no policies are found (ErrAPINotFound)
//
// Example:
//
// result, err := api.ListPolicies("", "")
// if err != nil {
// log.Printf("Error listing policies: %v", err)
// return
// }
// policies := *result
// for _, policy := range policies {
// log.Printf("Found policy: %+v", policy)
// }
func (a *API) ListPolicies(
SPIFFEIDPattern, pathPattern string,
) (*[]data.Policy, *sdkErrors.SDKError) {
return acl.ListPolicies(a.source, SPIFFEIDPattern, pathPattern)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/internal/impl/operator"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// Recover returns recovery partitions for SPIKE Nexus to be used in a
// break-the-glass recovery operation.
//
// This should be used when the SPIKE Nexus auto-recovery mechanism isn't
// successful. The returned shards are sensitive and should be securely stored
// out-of-band in encrypted form.
//
// Returns:
// - map[int]*[32]byte: Map of shard indices to shard byte arrays if
// successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Note: The function will fatally crash (via log.FatalErr) if:
// - SVID acquisition fails
// - SVID is nil
// - Caller is not SPIKE Pilot (security requirement)
//
// Example:
//
// shards, err := api.Recover()
// if err != nil {
// log.Fatalf("Failed to recover shards: %v", err)
// }
func (a *API) Recover() (map[int]*[32]byte, *sdkErrors.SDKError) {
return operator.Recover(a.source)
}
// Restore submits a recovery shard to continue the SPIKE Nexus restoration
// process.
//
// This is used when SPIKE Keepers cannot provide adequate shards and SPIKE
// Nexus cannot recall its root key. This is a break-the-glass superuser-only
// operation that a well-architected SPIKE deployment should not need.
//
// Parameters:
// - index: Index of the recovery shard
// - shard: Pointer to a 32-byte array containing the recovery shard
//
// Returns:
// - *data.RestorationStatus: Status containing shards collected, remaining,
// and restoration state if successful, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Note: The function will fatally crash (via log.FatalErr) if:
// - SVID acquisition fails
// - SVID is nil
// - Caller is not SPIKE Pilot (security requirement)
//
// Example:
//
// status, err := api.Restore(shardIndex, shardPtr)
// if err != nil {
// log.Fatalf("Failed to restore shard: %v", err)
// }
// log.Printf("Shards collected: %d, remaining: %d",
// status.ShardsCollected, status.ShardsRemaining)
func (a *API) Restore(
index int, shard *[32]byte,
) (*data.RestorationStatus, *sdkErrors.SDKError) {
return operator.Restore(a.source, index, shard)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\ SPDX-License-Identifier: Apache-2.0
package api
import (
"github.com/spiffe/spike-sdk-go/api/entity/data"
"github.com/spiffe/spike-sdk-go/api/internal/impl/secret"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// DeleteSecretVersions deletes specified versions of a secret at the given
// path.
//
// Parameters:
// - path: Path to the secret to delete
// - versions: Array of version numbers to delete
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := api.DeleteSecretVersions("secret/path", []int{1, 2})
// if err != nil {
// log.Printf("Failed to delete secret versions: %v", err)
// }
func (a *API) DeleteSecretVersions(
path string, versions []int,
) *sdkErrors.SDKError {
return secret.Delete(a.source, path, versions)
}
// DeleteSecret deletes the entire secret at the given path.
//
// Parameters:
// - path: Path to the secret to delete
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := api.DeleteSecret("secret/path")
// if err != nil {
// log.Printf("Failed to delete secret: %v", err)
// }
func (a *API) DeleteSecret(path string) *sdkErrors.SDKError {
return secret.Delete(a.source, path, []int{})
}
// GetSecretVersion retrieves a specific version of a secret at the given
// path.
//
// Parameters:
// - path: Path to the secret to retrieve
// - version: Version number of the secret to retrieve
//
// Returns:
// - *data.Secret: Secret data if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the secret is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// secret, err := api.GetSecretVersion("secret/path", 1)
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// log.Printf("Secret not found")
// return
// }
// log.Printf("Error retrieving secret: %v", err)
// return
// }
func (a *API) GetSecretVersion(
path string, version int,
) (*data.Secret, *sdkErrors.SDKError) {
return secret.Get(a.source, path, version)
}
// GetSecret retrieves the latest version of the secret at the given path.
//
// Parameters:
// - path: Path to the secret to retrieve
//
// Returns:
// - *data.Secret: Secret data if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the secret is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// secret, err := api.GetSecret("secret/path")
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// log.Printf("Secret not found")
// return
// }
// log.Printf("Error retrieving secret: %v", err)
// return
// }
func (a *API) GetSecret(path string) (*data.Secret, *sdkErrors.SDKError) {
return secret.Get(a.source, path, 0)
}
// ListSecretKeys retrieves all secret keys.
//
// Returns:
// - *[]string: Array of secret keys if found, empty array if none found,
// nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails (except ErrAPINotFound)
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Note: Returns (&[]string{}, nil) if no secrets are found (ErrAPINotFound)
//
// Example:
//
// keys, err := api.ListSecretKeys()
// if err != nil {
// log.Printf("Error listing keys: %v", err)
// return
// }
// for _, key := range *keys {
// log.Printf("Found key: %s", key)
// }
func (a *API) ListSecretKeys() (*[]string, *sdkErrors.SDKError) {
return secret.ListKeys(a.source)
}
// GetSecretMetadata retrieves metadata for a specific version of a secret at
// the given path.
//
// Parameters:
// - path: Path to the secret to retrieve metadata for
// - version: Version number of the secret to retrieve metadata for
//
// Returns:
// - *data.SecretMetadata: Secret metadata if found, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - ErrAPINotFound: if the secret metadata is not found
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// metadata, err := api.GetSecretMetadata("secret/path", 1)
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// log.Printf("Metadata not found")
// return
// }
// log.Printf("Error retrieving metadata: %v", err)
// return
// }
func (a *API) GetSecretMetadata(
path string, version int,
) (*data.SecretMetadata, *sdkErrors.SDKError) {
return secret.GetMetadata(a.source, path, version)
}
// PutSecret creates or updates a secret at the specified path with the given
// values.
//
// Parameters:
// - path: Path where the secret should be stored
// - data: Map of key-value pairs representing the secret data
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := api.PutSecret("secret/path", map[string]string{"key": "value"})
// if err != nil {
// log.Printf("Failed to put secret: %v", err)
// }
func (a *API) PutSecret(
path string, data map[string]string,
) *sdkErrors.SDKError {
return secret.Put(a.source, path, data)
}
// UndeleteSecret restores previously deleted versions of a secret at the
// specified path.
//
// Parameters:
// - path: Path to the secret to restore
// - versions: Array of version numbers to restore (empty array attempts no
// restoration)
//
// Returns:
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrSPIFFENilX509Source: if the X509 source is nil
// - ErrDataMarshalFailure: if request serialization fails
// - Errors from net.Post(): if the HTTP request fails
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the server returns an error
//
// Example:
//
// err := api.UndeleteSecret("secret/path", []int{1, 2})
// if err != nil {
// log.Printf("Failed to undelete secret: %v", err)
// }
func (a *API) UndeleteSecret(path string, versions []int) *sdkErrors.SDKError {
return secret.Undelete(a.source, path, versions)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// KeeperBootstrapContributeEndpoint constructs the full API endpoint URL for
// SPIKE Keeper contribution requests.
//
// It joins the provided keeper API root URL with the KeeperContribute path
// segment to create a complete endpoint URL for submitting secret shares to
// keepers.
//
// Parameters:
// - keeperAPIRoot: The base URL of the SPIKE Keeper API
//
// Returns:
// - string: The complete endpoint URL for keeper contribution requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := KeeperBootstrapContributeEndpoint("https://keeper.example.com")
func KeeperBootstrapContributeEndpoint(keeperAPIRoot string) string {
const fName = "KeeperBootstrapContributeEndpoint"
u, err := url.JoinPath(
keeperAPIRoot, string(KeeperContribute),
)
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Keeper API path"
log.FatalErr(fName, *failErr)
}
return u
}
// NexusBootstrapVerifyEndpoint constructs the full API endpoint URL for
// bootstrap verification requests.
//
// It joins the provided Nexus API root URL with the bootstrap verify path to
// create a complete endpoint URL for verifying that SPIKE Nexus has been
// properly initialized with the root key.
//
// Parameters:
// - nexusAPIRoot: The base URL of the SPIKE Nexus API
//
// Returns:
// - string: The complete endpoint URL for bootstrap verification requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := NexusBootstrapVerifyEndpoint("https://nexus.example.com")
func NexusBootstrapVerifyEndpoint(nexusAPIRoot string) string {
const fName = "NexusBootstrapVerifyEndpoint"
u, err := url.JoinPath(nexusAPIRoot, string(NexusBootstrapVerify))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus API path"
log.FatalErr(fName, *failErr)
}
return u
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// CipherEncrypt constructs the full API endpoint URL for encryption requests.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the cipher encrypt path to create a complete endpoint URL for encrypting
// data.
//
// Returns:
// - string: The complete endpoint URL for encryption requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := CipherEncrypt()
func CipherEncrypt() string {
const fName = "CipherEncrypt"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusCipherEncrypt))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus cipher encrypt path"
log.FatalErr(fName, *failErr)
}
return u
}
// CipherDecrypt constructs the full API endpoint URL for decryption requests.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the cipher decrypt path to create a complete endpoint URL for decrypting
// data.
//
// Returns:
// - string: The complete endpoint URL for decryption requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := CipherDecrypt()
func CipherDecrypt() string {
const fName = "CipherDecrypt"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusCipherDecrypt))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus cipher decrypt path"
log.FatalErr(fName, *failErr)
}
return u
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// Restore constructs the full API endpoint URL for operator restore requests.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the operator restore path to create a complete endpoint URL for submitting
// recovery shards during the restoration process.
//
// Returns:
// - string: The complete endpoint URL for operator restore requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := Restore()
func Restore() string {
const fName = "Restore"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusOperatorRestore))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus operator restore path"
log.FatalErr(fName, *failErr)
}
return u
}
// Recover constructs the full API endpoint URL for operator recover requests.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the operator recover path to create a complete endpoint URL for initiating
// the recovery process and retrieving recovery shards.
//
// Returns:
// - string: The complete endpoint URL for operator recover requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := Recover()
func Recover() string {
const fName = "Recover"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusOperatorRecover))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus operator recover path"
log.FatalErr(fName, *failErr)
}
return u
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// PolicyCreate constructs the full API endpoint URL for creating policies.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the policy path to create a complete endpoint URL for creating new policies.
//
// Returns:
// - string: The complete endpoint URL for policy creation requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := PolicyCreate()
func PolicyCreate() string {
const fName = "PolicyCreate"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus policy path"
log.FatalErr(fName, *failErr)
}
return u
}
// PolicyList constructs the full API endpoint URL for listing policies.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the policy path and adds query parameters to specify the list action.
//
// Returns:
// - string: The complete endpoint URL for policy list requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := PolicyList()
func PolicyList() string {
const fName = "PolicyList"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus policy path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionList))
return u + "?" + params.Encode()
}
// PolicyDelete constructs the full API endpoint URL for deleting policies.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the policy path and adds query parameters to specify the delete action.
//
// Returns:
// - string: The complete endpoint URL for policy deletion requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := PolicyDelete()
func PolicyDelete() string {
const fName = "PolicyDelete"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus policy path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionDelete))
return u + "?" + params.Encode()
}
// PolicyGet constructs the full API endpoint URL for retrieving a policy.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the policy path and adds query parameters to specify the get action.
//
// Returns:
// - string: The complete endpoint URL for policy retrieval requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := PolicyGet()
func PolicyGet() string {
const fName = "PolicyGet"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusPolicy))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus policy path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionGet))
return u + "?" + params.Encode()
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// SecretGet constructs the full API endpoint URL for retrieving secrets.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets path and adds query parameters to specify the get action.
//
// Returns:
// - string: The complete endpoint URL for secret retrieval requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretGet()
func SecretGet() string {
const fName = "SecretGet"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionGet))
return u + "?" + params.Encode()
}
// SecretPut constructs the full API endpoint URL for creating or updating
// secrets.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets path to create a complete endpoint URL for storing secrets.
//
// Returns:
// - string: The complete endpoint URL for secret creation/update requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretPut()
func SecretPut() string {
const fName = "SecretPut"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets path"
log.FatalErr(fName, *failErr)
}
return u
}
// SecretDelete constructs the full API endpoint URL for deleting secrets.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets path and adds query parameters to specify the delete action.
//
// Returns:
// - string: The complete endpoint URL for secret deletion requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretDelete()
func SecretDelete() string {
const fName = "SecretDelete"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionDelete))
return u + "?" + params.Encode()
}
// SecretUndelete constructs the full API endpoint URL for restoring deleted
// secrets.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets path and adds query parameters to specify the undelete action.
//
// Returns:
// - string: The complete endpoint URL for secret restoration requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretUndelete()
func SecretUndelete() string {
const fName = "SecretUndelete"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionUndelete))
return u + "?" + params.Encode()
}
// SecretList constructs the full API endpoint URL for listing secrets.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets path and adds query parameters to specify the list action.
//
// Returns:
// - string: The complete endpoint URL for secret list requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretList()
func SecretList() string {
const fName = "SecretList"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecrets))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionList))
return u + "?" + params.Encode()
}
// SecretMetadataGet constructs the full API endpoint URL for retrieving secret
// metadata.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the secrets metadata path and adds query parameters to specify the get action.
//
// Returns:
// - string: The complete endpoint URL for secret metadata retrieval requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := SecretMetadataGet()
func SecretMetadataGet() string {
const fName = "SecretMetadataGet"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusSecretsMetadata))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus secrets metadata path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionGet))
return u + "?" + params.Encode()
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package url
import (
"net/url"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// Init constructs the full API endpoint URL for initializing SPIKE Nexus.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the init path to create a complete endpoint URL for initializing SPIKE Nexus
// with the root encryption key.
//
// Returns:
// - string: The complete endpoint URL for initialization requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := Init()
func Init() string {
const fName = "Init"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusInit))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus init path"
log.FatalErr(fName, *failErr)
}
return u
}
// InitState constructs the full API endpoint URL for checking the
// initialization state of SPIKE Nexus.
//
// It joins the SPIKE Nexus API root URL (from environment configuration) with
// the init path and adds query parameters to specify the check action for
// verifying whether SPIKE Nexus has been initialized.
//
// Returns:
// - string: The complete endpoint URL for initialization state check requests
//
// Note: The function will fatally crash (via log.FatalErr) if URL path joining
// fails.
//
// Example:
//
// endpoint := InitState()
func InitState() string {
const fName = "InitState"
u, err := url.JoinPath(env.NexusAPIRootVal(), string(NexusInit))
if err != nil {
failErr := sdkErrors.ErrNetURLJoinPathFailed.Wrap(err)
failErr.Msg = "failed to join SPIKE Nexus init path"
log.FatalErr(fName, *failErr)
}
params := url.Values{}
params.Add(KeyAPIAction, string(ActionCheck))
return u + "?" + params.Encode()
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strings"
)
// StoreType represents the type of backend storage to use.
type StoreType string
const (
// Lite mode
// This mode converts SPIKE to an encryption-as-a-service app.
// It is used to store secrets in S3-compatible mediums (such as Minio)
// without actually persisting them to a backing store.
// In this mode SPIKE policies are "minimally" enforced, and the recommended
// way to manage RBAC is to use the object storage's policy rules instead.
Lite StoreType = "lite"
// Sqlite indicates a SQLite database storage backend
// This is the default backing store. SPIKE_NEXUS_BACKEND_STORE environment
// variable can override it.
Sqlite StoreType = "sqlite"
// Memory indicates an in-memory storage backend
// This mode is not recommended for production use as SPIKE will NOT rely on
// SPIKE Keeper instances for Disaster Recovery and Redundancy.
Memory StoreType = "memory"
)
// BackendStoreTypeVal determines which storage backend type to use based on the
// SPIKE_NEXUS_BACKEND_STORE environment variable. The value is
// case-insensitive.
//
// Valid values are:
// - "lite": Lite mode that does not use any backing store
// - "sqlite": Uses SQLite database storage
// - "memory": Uses in-memory storage
//
// If the environment variable is not set or contains an invalid value,
// it defaults to SQLite.
func BackendStoreTypeVal() StoreType {
st := os.Getenv(NexusBackendStore)
switch strings.ToLower(st) {
case string(Lite):
return Lite
case string(Sqlite):
return Sqlite
case string(Memory):
return Memory
default:
return Sqlite
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import "os"
// BootstrapConfigMapNameVal returns the name of the ConfigMap used to store
// SPIKE Bootstrap state information.
//
// It retrieves the ConfigMap name from the SPIKE_BOOTSTRAP_CONFIGMAP_NAME
// environment variable. If the environment variable is not set, it returns
// the default value "spike-bootstrap-state".
//
// Returns:
// - A string containing the ConfigMap name for storing bootstrap state
func BootstrapConfigMapNameVal() string {
cn := os.Getenv(BootstrapConfigMapName)
if cn == "" {
return "spike-bootstrap-state"
}
return cn
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
)
// CryptoMaxCiphertextSizeVal returns the maximum allowed ciphertext size in
// bytes.
// It reads the value from the SPIKE_NEXUS_CRYPTO_MAX_CIPHERTEXT_SIZE
// environment variable. If the variable is not set or contains an invalid
// value, it defaults to 65536 bytes (64 KB).
func CryptoMaxCiphertextSizeVal() int {
p := os.Getenv(NexusCryptoMaxCiphertextSize)
if p != "" {
mv, err := strconv.Atoi(p)
if err == nil && mv > 0 {
return mv
}
}
return 65536
}
// CryptoMaxPlaintextSizeVal returns the maximum allowed plaintext size in
// bytes.
// It is calculated as CryptoMaxCiphertextSizeVal minus 16 bytes, which accounts
// for the authentication tag overhead used in authenticated encryption schemes
// such as AES-GCM.
func CryptoMaxPlaintextSizeVal() int {
return CryptoMaxCiphertextSizeVal() - 16
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
"time"
)
// DatabaseJournalModeVal returns the SQLite journal mode to use.
// It can be configured using the SPIKE_NEXUS_DB_JOURNAL_MODE environment
// variable.
//
// If the environment variable is not set, it defaults to "WAL"
// (Write-Ahead Logging).
func DatabaseJournalModeVal() string {
s := os.Getenv(NexusDBJournalMode)
if s != "" {
return s
}
return "WAL"
}
// DatabaseBusyTimeoutMsVal returns the SQLite busy timeout in milliseconds.
// It can be configured using the SPIKE_NEXUS_DB_BUSY_TIMEOUT_MS environment
// variable. The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 5000 milliseconds (5 seconds).
func DatabaseBusyTimeoutMsVal() int {
p := os.Getenv(NexusDBBusyTimeoutMS)
if p != "" {
bt, err := strconv.Atoi(p)
if err == nil && bt > 0 {
return bt
}
}
return 5000
}
// DatabaseMaxOpenConnsVal returns the maximum number of open database
// connections. It can be configured using the SPIKE_NEXUS_DB_MAX_OPEN_CONNS
// environment variable. The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 10 connections.
func DatabaseMaxOpenConnsVal() int {
p := os.Getenv(NexusDBMaxOpenConns)
if p != "" {
moc, err := strconv.Atoi(p)
if err == nil && moc > 0 {
return moc
}
}
return 10
}
// DatabaseMaxIdleConnsVal returns the maximum number of idle database
// connections. It can be configured using the SPIKE_NEXUS_DB_MAX_IDLE_CONNS
// environment variable. The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 5 connections.
func DatabaseMaxIdleConnsVal() int {
p := os.Getenv(NexusDBMaxIdleConns)
if p != "" {
mic, err := strconv.Atoi(p)
if err == nil && mic > 0 {
return mic
}
}
return 5
}
// DatabaseConnMaxLifetimeSecVal returns the maximum lifetime duration for a
// database connection. It can be configured using the
// SPIKE_NEXUS_DB_CONN_MAX_LIFETIME environment variable.
// The value should be a valid Go duration string (e.g., "1h", "30m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 1 hour.
func DatabaseConnMaxLifetimeSecVal() time.Duration {
p := os.Getenv(NexusDBConnMaxLifetime)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return time.Hour
}
// DatabaseOperationTimeoutVal returns the duration to use for database
// operations. It can be configured using the SPIKE_NEXUS_DB_OPERATION_TIMEOUT
// environment variable. The value should be a valid Go duration string
// (e.g., "10s", "1m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 15 seconds.
func DatabaseOperationTimeoutVal() time.Duration {
p := os.Getenv(NexusDBOperationTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 15 * time.Second
}
// DatabaseInitializationTimeoutVal returns the duration to wait for database
// initialization.
//
// The timeout is read from the environment variable
// `SPIKE_NEXUS_DB_INITIALIZATION_TIMEOUT`. If this variable is set and its
// value can be parsed as a duration (e.g., "1m30s"), it is used.
// Otherwise, the function defaults to a timeout of 30 seconds.
func DatabaseInitializationTimeoutVal() time.Duration {
p := os.Getenv(NexusDBInitializationTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 30 * time.Second
}
// DatabaseSkipSchemaCreationVal determines if schema creation should be
// skipped. It checks the "SPIKE_NEXUS_DB_SKIP_SCHEMA_CREATION" env variable
// to decide.
// If the env variable is set and its value is "true", it returns true.
// Otherwise, it returns false.
func DatabaseSkipSchemaCreationVal() bool {
p := os.Getenv(NexusDBSkipSchemaCreation)
if p != "" {
s, err := strconv.ParseBool(p)
if err == nil {
return s
}
}
return false
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
"time"
)
// HTTPClientDialerKeepAliveVal returns the keep-alive duration for the HTTP
// client's dialer. It can be configured using the
// SPIKE_HTTP_CLIENT_DIALER_KEEP_ALIVE environment variable.
// The value should be a valid Go duration string (e.g., "30s", "1m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 30 seconds.
func HTTPClientDialerKeepAliveVal() time.Duration {
p := os.Getenv(HTTPClientDialerKeepAlive)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 30 * time.Second
}
// HTTPClientDialerTimeoutVal returns the timeout duration for the HTTP
// client's dialer. It can be configured using the
// SPIKE_HTTP_CLIENT_DIALER_TIMEOUT environment variable.
// The value should be a valid Go duration string (e.g., "30s", "1m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 30 seconds.
func HTTPClientDialerTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientDialerTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 30 * time.Second
}
// HTTPClientExpectContinueTimeoutVal returns the timeout for Expect: 100-continue
// responses from the server. It can be configured using the
// SPIKE_HTTP_CLIENT_EXPECT_CONTINUE_TIMEOUT environment variable.
// The value should be a valid Go duration string (e.g., "5s", "10s").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 5 seconds.
func HTTPClientExpectContinueTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientExpectContinueTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 5 * time.Second
}
// HTTPClientIdleConnTimeoutVal returns the maximum duration an idle connection
// will remain idle before closing. It can be configured using the
// SPIKE_HTTP_CLIENT_IDLE_CONN_TIMEOUT environment variable.
// The value should be a valid Go duration string (e.g., "30s", "1m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 30 seconds.
func HTTPClientIdleConnTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientIdleConnTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 30 * time.Second
}
// HTTPClientMaxConnsPerHostVal returns the maximum number of connections
// per host. It can be configured using the SPIKE_HTTP_CLIENT_MAX_CONNS_PER_HOST
// environment variable. The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 10 connections.
func HTTPClientMaxConnsPerHostVal() int {
p := os.Getenv(HTTPClientMaxConnsPerHost)
if p != "" {
moc, err := strconv.Atoi(p)
if err == nil && moc > 0 {
return moc
}
}
return 10
}
// HTTPClientMaxIdleConnsVal returns the maximum number of idle connections
// across all hosts. It can be configured using the
// SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS environment variable.
// The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 100 connections.
func HTTPClientMaxIdleConnsVal() int {
p := os.Getenv(HTTPClientMaxIdleConns)
if p != "" {
mic, err := strconv.Atoi(p)
if err == nil && mic > 0 {
return mic
}
}
return 100
}
// HTTPClientMaxIdleConnsPerHostVal returns the maximum number of idle
// connections per host. It can be configured using the
// SPIKE_HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST environment variable.
// The value must be a positive integer.
//
// If the environment variable is not set or contains an invalid value,
// it defaults to 10 connections.
func HTTPClientMaxIdleConnsPerHostVal() int {
p := os.Getenv(HTTPClientMaxIdleConnsPerHost)
if p != "" {
mic, err := strconv.Atoi(p)
if err == nil && mic > 0 {
return mic
}
}
return 10
}
// HTTPClientResponseHeaderTimeoutVal returns the timeout for waiting for a
// server's response headers. It can be configured using the
// SPIKE_HTTP_CLIENT_RESPONSE_HEADER_TIMEOUT environment variable.
// The value should be a valid Go duration string (e.g., "10s", "30s").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 10 seconds.
func HTTPClientResponseHeaderTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientResponseHeaderTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 10 * time.Second
}
// HTTPClientTimeoutVal returns the overall timeout for HTTP client requests.
// It can be configured using the SPIKE_HTTP_CLIENT_TIMEOUT environment variable.
// The value should be a valid Go duration string (e.g., "60s", "2m").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 60 seconds.
func HTTPClientTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 60 * time.Second
}
// HTTPClientTLSHandshakeTimeoutVal returns the timeout for the TLS handshake.
// It can be configured using the SPIKE_HTTP_CLIENT_TLS_HANDSHAKE_TIMEOUT
// environment variable. The value should be a valid Go duration string
// (e.g., "10s", "30s").
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 10 seconds.
func HTTPClientTLSHandshakeTimeoutVal() time.Duration {
p := os.Getenv(HTTPClientTLSHandshakeTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 10 * time.Second
}
// HTTPServerReadHeaderTimeoutVal returns the timeout for reading HTTP request
// headers on the server side. It can be configured using the
// SPIKE_HTTP_SERVER_READ_HEADER_TIMEOUT environment variable. The value should
// be a valid Go duration string (e.g., "10s", "30s").
//
// This timeout helps prevent slowloris attacks by limiting how long the server
// will wait for request headers to be sent by the client.
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 10 seconds.
func HTTPServerReadHeaderTimeoutVal() time.Duration {
p := os.Getenv(HTTPServerReadHeaderTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 10 * time.Second
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
"strings"
"github.com/spiffe/spike-sdk-go/log"
)
// KeepersVal retrieves and parses the keeper peer configurations from the
// environment. It reads SPIKE_NEXUS_KEEPER_PEERS environment variable which
// should contain a comma-separated list of keeper URLs.
//
// The environment variable should be formatted as:
// 'https://localhost:8443,https://localhost:8543,https://localhost:8643'
//
// The SPIKE Keeper address mappings will be automatically assigned starting
// with the key "1" and incrementing by 1 for each subsequent SPIKE Keeper.
//
// Returns:
// - map[string]string: Mapping of keeper IDs to their URLs
//
// Panics if:
// - SPIKE_NEXUS_KEEPER_PEERS is not set
func KeepersVal() map[string]string {
const fName = "KeepersVal"
p := os.Getenv(NexusKeeperPeers)
if p == "" {
log.FatalLn(
fName,
"message",
"SPIKE_NEXUS_KEEPER_PEERS must be configured in the environment",
)
}
urls := strings.Split(p, ",")
// Check for duplicate and empty URLs
urlMap := make(map[string]bool)
for i, u := range urls {
trimmedURL := strings.TrimSpace(u)
if trimmedURL == "" {
log.FatalLn(
fName,
"message", "empty url found",
"position", i+1,
)
}
// Validate URL format and security
if !validURL(trimmedURL) {
log.FatalLn(
fName,
"message", "invalid url format",
"position", i+1,
)
}
if urlMap[trimmedURL] {
log.FatalLn(
fName,
"message", "duplicate url found",
"position", i+1,
)
}
urlMap[trimmedURL] = true
}
// The key of the map is the Shamir Shard index (starting from 1), and
// the value is the Keeper URL that corresponds to that shard index.
peers := make(map[string]string)
for i, u := range urls {
peers[strconv.Itoa(i+1)] = strings.TrimSpace(u)
}
return peers
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import "os"
// NexusTLSPortVal returns the TLS port for the Spike Nexus service.
// It reads from the SPIKE_NEXUS_TLS_PORT environment variable.
// If the environment variable is not set, it returns the default port ":8553".
func NexusTLSPortVal() string {
p := os.Getenv(NexusTLSPort)
if p != "" {
return p
}
return ":8553"
}
// KeeperTLSPortVal returns the TLS port for the Spike Keeper service.
// It first checks for a port specified in the SPIKE_KEEPER_TLS_PORT
// environment variable.
// If no environment variable is set, it defaults to ":8443".
//
// The returned string is in the format ":port" suitable for use with
// net/http Listen functions.
func KeeperTLSPortVal() string {
p := os.Getenv(KeeperTLSPort)
if p != "" {
return p
}
return ":8443"
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strings"
)
// BannerEnabledVal returns whether to show the initial banner on app start
// based on the SPIKE_BANNER_ENABLED environment variable.
//
// The function reads the SPIKE_BANNER_ENABLED environment variable and returns:
// - true if the variable is not set (default behavior)
// - true if the variable is set to "true" (case-insensitive)
// - false for any other value
//
// The environment variable value is trimmed of whitespace and converted to
// lowercase before comparison.
func BannerEnabledVal() bool {
s := os.Getenv(BannerEnabled)
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return true
}
return s == "true"
}
// ShowMemoryWarningVal returns whether to display a warning when the system
// cannot lock memory based on the SPIKE_PILOT_SHOW_MEMORY_WARNING environment
// variable.
//
// The function reads the SPIKE_PILOT_SHOW_MEMORY_WARNING environment variable
// and returns:
// - false if the variable is not set (default behavior)
// - true if the variable is set to "true" (case-insensitive)
// - false for any other value
//
// The environment variable value is trimmed of whitespace and converted to
// lowercase before comparison.
//
// This warning is typically shown when memory locking fails, which could
// impact security-sensitive operations that require pages to remain in RAM.
func ShowMemoryWarningVal() bool {
s := os.Getenv(PilotShowMemoryWarning)
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return false
}
return s == "true"
}
// StackTracesOnLogFatalVal returns whether to print stack traces when
// `log.FatalLn` is called, based on the SPIKE_STACK_TRACES_ON_LOG_FATAL
// environment variable.
//
// The function reads the SPIKE_STACK_TRACES_ON_LOG_FATAL environment variable
// and returns:
// - false if the variable is not set (default behavior - clean exit)
// - true if the variable is set to "true" (case-insensitive - panic with stack trace)
// - false for any other value
//
// The environment variable value is trimmed of whitespace and converted to
// lowercase before comparison.
//
// By default, log.FatalLn performs a clean exit to avoid leaking sensitive
// information in production stack traces. Enable this for development and
// testing purposes.
func StackTracesOnLogFatalVal() bool {
s := os.Getenv(StackTracesOnLogFatal)
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return false
}
return s == "true"
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"time"
)
// RecoveryOperationMaxIntervalVal returns the maximum interval duration for
// recovery backoff retry algorithm. The interval is determined by the
// environment variable `SPIKE_NEXUS_RECOVERY_MAX_INTERVAL`.
//
// If the environment variable is not set or is not a valid duration
// string, then it defaults to 60 seconds.
func RecoveryOperationMaxIntervalVal() time.Duration {
e := os.Getenv(NexusRecoveryMaxInterval)
if e != "" {
if d, err := time.ParseDuration(e); err == nil {
return d
}
}
return 60 * time.Second
}
// RecoveryKeeperUpdateIntervalVal returns the duration between keeper updates
// for SPIKE Nexus. It first attempts to read the duration from the
// SPIKE_NEXUS_KEEPER_UPDATE_INTERVAL environment variable. If the environment
// variable is set and contains a valid duration string (as parsed by
// time.ParseDuration), that duration is returned. Otherwise, it returns a
// default value of 5 minutes.
func RecoveryKeeperUpdateIntervalVal() time.Duration {
e := os.Getenv(NexusKeeperUpdateInterval)
if e != "" {
if d, err := time.ParseDuration(e); err == nil {
return d
}
}
return 5 * time.Minute
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
)
// MaxSecretVersionsVal returns the maximum number of versions to retain
// for each secret. It reads from the SPIKE_NEXUS_MAX_SECRET_VERSIONS
// environment variable which should contain a positive integer value.
// If the environment variable is not set, contains an invalid integer, or
// specifies a non-positive value, it returns the default of 10 versions.
func MaxSecretVersionsVal() int {
p := os.Getenv(NexusMaxEntryVersions)
if p != "" {
mv, err := strconv.Atoi(p)
if err == nil && mv > 0 {
return mv
}
}
return 10
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strconv"
)
// ShamirSharesVal returns the total number of shares to be used in Shamir's
// Secret Sharing. It reads the value from the SPIKE_NEXUS_SHAMIR_SHARES
// environment variable.
//
// Returns:
// - The number of shares specified in the environment variable if it's a
// valid positive integer
// - The default value of 3 if the environment variable is unset, empty,
// or invalid
//
// This determines the total number of shares that will be created when
//
// splitting a secret.
func ShamirSharesVal() int {
p := os.Getenv(NexusShamirShares)
if p != "" {
mv, err := strconv.Atoi(p)
if err == nil && mv > 0 {
return mv
}
}
return 3
}
// ShamirMaxShareCountVal returns the maximum allowed number of shares in
// Shamir's Secret Sharing scheme. It reads the value from the
// SPIKE_NEXUS_SHAMIR_MAX_SHARE_COUNT environment variable.
//
// Returns:
// - The maximum share count specified in the environment variable if it's
// a valid positive integer
// - The default value of 1000 if the environment variable is unset, empty,
// or invalid
//
// This limit prevents excessive resource consumption when creating shares.
// This variable also limits the maximum number of SPIKE Keeper instances that
// a SPIKE deployment can support.
func ShamirMaxShareCountVal() int {
p := os.Getenv(NexusShamirMaxShareCount)
if p != "" {
mv, err := strconv.Atoi(p)
if err == nil && mv > 0 {
return mv
}
}
return 1000
}
// ShamirThresholdVal returns the minimum number of shares required to
// reconstruct the secret in Shamir's Secret Sharing scheme.
// It reads the value from the SPIKE_NEXUS_SHAMIR_THRESHOLD environment
// variable.
//
// Returns:
// - The threshold specified in the environment variable if it's a valid
// positive integer
// - The default value of 2 if the environment variable is unset, empty,
// or invalid
//
// This threshold value determines how many shares are needed to recover the
// original secret. It should be less than or equal to the total number of
// shares (ShamirShares()).
func ShamirThresholdVal() int {
p := os.Getenv(NexusShamirThreshold)
if p != "" {
mv, err := strconv.Atoi(p)
if err == nil && mv > 0 {
return mv
}
}
return 2
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"time"
)
// SPIFFESourceTimeoutVal returns the timeout duration for creating a SPIFFE
// X509Source and fetching the initial SVID from the SPIFFE Workload API.
// It can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT environment
// variable. The value should be a valid Go duration string (e.g., "30s", "1m").
//
// This timeout prevents indefinite blocking if there are issues with the
// SPIFFE Workload API socket (e.g., agent not running, socket permissions,
// network issues).
//
// If the environment variable is not set or contains an invalid duration,
// it defaults to 30 seconds.
func SPIFFESourceTimeoutVal() time.Duration {
p := os.Getenv(SPIFFESourceTimeout)
if p != "" {
d, err := time.ParseDuration(p)
if err == nil {
return d
}
}
return 30 * time.Second
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import (
"os"
"strings"
)
const defaultTrustRoot = "spike.ist"
// TrustRootFromEnv retrieves the trust root from an environment variable.
// It takes the name of an environment variable and returns its value.
// The environment variable name must start with "SPIKE_TRUST_ROOT" for
// security. If the environment variable name doesn't follow this pattern,
// is not set, or is empty, it returns the default trust root "spike.ist".
//
// Parameters:
// - trustRootEnvVar: The name of the environment variable to read
// (must start with "SPIKE_TRUST_ROOT")
//
// Returns:
// - The value of the environment variable, or "spike.ist" if not set or
// invalid name
func TrustRootFromEnv(trustRootEnvVar string) string {
// Validate that the environment variable follows the expected pattern.
// If the pattern does not match, return the default trust root.
if !strings.HasPrefix(trustRootEnvVar, TrustRoot) {
return defaultTrustRoot
}
tr := os.Getenv(trustRootEnvVar)
if tr == "" {
return defaultTrustRoot
}
return tr
}
// TrustRootVal returns the default trust root from the SPIKE_TRUST_ROOT
// environment variable. This is a convenience function that calls
// TrustRootFromEnv with the default trust root environment variable name.
//
// Returns:
// - The value of SPIKE_TRUST_ROOT environment variable, or "spike.ist"
// if not set
func TrustRootVal() string {
return TrustRootFromEnv(TrustRoot)
}
// TrustRootForKeeperVal returns the trust root for SPIKE Keeper from the
// SPIKE_TRUST_ROOT_KEEPER environment variable. This is a convenience function
// that calls TrustRootFromEnv with the Keeper-specific environment variable.
//
// Returns:
// - The value of SPIKE_TRUST_ROOT_KEEPER environment variable, or "spike.ist"
// if not set
func TrustRootForKeeperVal() string {
return TrustRootFromEnv(TrustRootKeeper)
}
// TrustRootForPilotVal returns the trust root for SPIKE Pilot from the
// SPIKE_TRUST_ROOT_PILOT environment variable. This is a convenience function
// that calls TrustRootFromEnv with the Pilot-specific environment variable.
//
// Returns:
// - The value of SPIKE_TRUST_ROOT_PILOT environment variable, or "spike.ist"
// if not set
func TrustRootForPilotVal() string {
return TrustRootFromEnv(TrustRootPilot)
}
// TrustRootForLiteWorkloadVal returns the trust root for SPIKE Lite Workloads
// from the SPIKE_TRUST_ROOT_LITE_WORKLOAD environment variable. This is a
// convenience function that calls TrustRootFromEnv with the Lite
// Workload-specific environment variable.
//
// Returns:
// - The value of SPIKE_TRUST_ROOT_LITE_WORKLOAD environment variable, or
// "spike.ist" if not set
func TrustRootForLiteWorkloadVal() string {
return TrustRootFromEnv(TrustRootLiteWorkload)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import "os"
const nexusDefaultAPIRoot = "https://localhost:8553"
// NexusAPIRootVal retrieves the SPIKE Nexus API root URL from the environment.
// It reads the value from the SPIKE_NEXUS_API_URL environment variable.
//
// Returns:
// - The Nexus API root URL from the environment variable if set
// - The default value of "https://localhost:8553" if the environment
// variable is unset or empty
func NexusAPIRootVal() string {
apiRoot := os.Getenv(NexusAPIURL)
if apiRoot == "" {
return nexusDefaultAPIRoot
}
return apiRoot
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package env
import "net/url"
// validURL validates that a URL is properly formatted and uses HTTPS
func validURL(urlStr string) bool {
pu, err := url.Parse(urlStr)
if err != nil {
return false
}
return pu.Scheme == "https" && pu.Host != ""
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package crypto
import (
"crypto/rand"
"encoding/hex"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
var reader = rand.Read
// AES256KeySize defines the size of a key in bytes for AES-256 encryption.
const AES256KeySize = 32
// AES256Seed generates a cryptographically secure random 256-bit key suitable
// for use with AES-256 encryption. The key is returned as a hexadecimal-encoded
// string.
//
// Returns:
// - string: A 64-character hexadecimal string representing the 256-bit key,
// empty string on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrCryptoFailedToCreateCipher: if random key generation fails
//
// The function uses a cryptographically secure random number generator to
// ensure the generated key is suitable for cryptographic use. The resulting hex
// string can be decoded back to bytes using hex.DecodeString when needed for
// encryption.
func AES256Seed() (string, *sdkErrors.SDKError) {
// Generate a 256-bit key
key := make([]byte, AES256KeySize)
_, err := reader(key)
if err != nil {
failErr := sdkErrors.ErrCryptoFailedToCreateCipher.Wrap(err)
failErr.Msg = "failed to generate random key"
return "", failErr
}
return hex.EncodeToString(key), nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package crypto
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
const letters = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// RandomString generates a cryptographically secure random string of the
// specified length using alphanumeric characters (a-z, A-Z, 0-9).
//
// Security Note: This function will fatally crash the process
// (via log.FatalErr) if the system's cryptographic random number generator
// fails. This is an intentional security decision, not a bug. Here's why:
//
// 1. CSPRNG failure indicates a critical system-level security compromise
// 2. Continuing with potentially weak/predictable random values would create
// security vulnerabilities (weak tokens, predictable IDs, compromised
// secrets)
// 3. There is no safe fallback - using non-cryptographic randomness or
// deterministic values would be catastrophically insecure
// 4. This is consistent with other security-critical failures in the codebase
// (SVID acquisition, Shamir operations, Pilot restrictions)
// 5. Failing loudly prevents silent security degradation and forces operators
// to address the underlying system issue
//
// The crypto/rand documentation states that Read failures are extremely rare
// and indicate serious OS-level problems. When this happens, the entire
// system's security is compromised, not just this function.
//
// Parameters:
// - n: length of the random string to generate
//
// Returns:
// - string: the generated random alphanumeric string
func RandomString(n int) string {
const fName = "RandomString"
bytes := make([]byte, n)
if _, err := reader(bytes); err != nil {
failErr := sdkErrors.ErrCryptoRandomGenerationFailed.Wrap(err)
failErr.Msg = "cryptographic random number generator failed"
log.FatalErr(fName, *failErr)
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes)
}
// Token generates a cryptographically secure random token with the "spike."
// prefix. The token consists of the prefix followed by 26 random alphanumeric
// characters, resulting in a format like "spike.AbCd1234EfGh5678IjKl9012Mn".
//
// Security Note: This function will fatally crash the process if the
// cryptographic random number generator fails. See RandomString() documentation
// for the security rationale behind this behavior.
//
// Returns:
// - string: the generated token in the format "spike.<26-char-random-string>"
func Token() string {
id := RandomString(26)
return "spike." + id
}
// ID generates a cryptographically secure random identifier consisting of
// 8 alphanumeric characters. Suitable for use as short, unique identifiers.
//
// Security Note: This function will fatally crash the process if the
// cryptographic random number generator fails. See RandomString() documentation
// for the security rationale behind this behavior.
//
// Returns:
// - string: the generated 8-character random alphanumeric string
func ID() string {
return RandomString(8)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package crypto
import (
"crypto/sha256"
)
// DeterministicReader implements io.Reader to generate deterministic
// pseudo-random data based on a seed. It uses SHA-256 hashing to create a
// repeatable stream of bytes.
type DeterministicReader struct {
data []byte
pos int
}
// Read implements io.Reader interface. It returns deterministic data by reading
// from the internal buffer and generating new data using SHA-256 when needed.
//
// If the current position reaches the end of the data buffer, it generates
// a new block by hashing the current data. This ensures a continuous,
// deterministic stream of data.
//
// This implementation properly satisfies the io.Reader interface contract.
// The error return is always nil since deterministic hashing operations cannot
// fail, but is required for io.Reader interface compliance.
//
// Parameters:
// - p []byte: Buffer to read data into
//
// Returns:
// - n int: Number of bytes read
// - err error: Always nil (deterministic reads never fail)
func (r *DeterministicReader) Read(p []byte) (n int, err error) {
if r.pos >= len(r.data) {
// Generate more deterministic data if needed
hash := sha256.Sum256(r.data)
r.data = hash[:]
r.pos = 0
}
n = copy(p, r.data[r.pos:])
r.pos += n
return n, nil
}
// NewDeterministicReader creates a new DeterministicReader initialized with
// the SHA-256 hash of the provided seed data.
//
// Parameters:
// - seed []byte: Initial seed data to generate the deterministic stream
//
// Returns:
// - *DeterministicReader: New reader instance initialized with the seed
func NewDeterministicReader(seed []byte) *DeterministicReader {
hash := sha256.Sum256(seed)
return &DeterministicReader{
data: hash[:],
pos: 0,
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package errors
// FromCode maps an ErrorCode to its corresponding SDKError using the
// automatically populated error registry. This is used to convert error codes
// received from API responses back to proper SDKError instances.
//
// The registry is automatically populated when errors are defined using the
// register() function, ensuring new errors are immediately available without
// manual updates to this function.
//
// If the error code is not recognized, it returns ErrGeneralFailure.
//
// Parameters:
// - code: the error code to map
//
// Returns:
// - *SDKError: the corresponding SDK error instance
func FromCode(code ErrorCode) *SDKError {
// Defensive coding: While concurrent reads to a map are safe, unless a
// write happens concurrently; if we enable dynamic error registration
// later down the line, without a mutex the behavior of this code will be
// undeterministic.
errorRegistryMu.RLock()
err, ok := errorRegistry[code]
errorRegistryMu.RUnlock()
if ok {
return err
}
return ErrGeneralFailure
}
// MaybeError converts an error to its string representation if the error is
// not nil. If the error is nil, it returns an empty string.
//
// Parameters:
// - err: the error to convert to a string
//
// Returns:
// - string: the error message if err is non-nil, empty string otherwise
func MaybeError(err error) string {
if err != nil {
return err.Error()
}
return ""
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package errors
import "sync"
type ErrorCode string
// errorRegistry maps ErrorCodes to their corresponding SDKError instances.
// This map is automatically populated when errors are defined using the
// register() function, ensuring FromCode() always has up-to-date mappings.
// Access is protected by errorRegistryMu for thread safety.
var (
errorRegistry = make(map[ErrorCode]*SDKError)
errorRegistryMu sync.RWMutex
)
// register creates a new SDKError, adds it to the global registry, and
// returns it. This ensures that all defined errors are automatically available
// in FromCode(). This function is thread-safe.
//
// Parameters:
// - code: The error code string
// - msg: The human-readable error message
// - wrapped: Optional wrapped error (typically nil for predefined errors)
//
// Returns:
// - *SDKError: The newly created and registered error
func register(code string, msg string, wrapped error) *SDKError {
err := New(ErrorCode(code), msg, wrapped)
errorRegistryMu.Lock()
errorRegistry[err.Code] = err
errorRegistryMu.Unlock()
return err
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package errors
import (
"errors"
"fmt"
)
// SDKError represents a structured error in the SPIKE SDK. It provides error
// codes for programmatic handling, human-readable messages, and support for
// error wrapping to maintain error chains.
//
// Usage patterns (see ADR-0028):
// 1. All SDK and non-CLI SPIKE errors shall use SDKError
// 2. All comparisons shall be done with errors.Is()
// 3. Context information shall be included in the Msg field
// 4. Import SDK errors as `sdkErrors` consistently for easier code search
// 5. Use predefined errors and wrap them with context, don't create from codes
//
// Example:
//
// // Use predefined errors
// return sdkErrors.ErrEntityNotFound
//
// // Or wrap with additional context
// return sdkErrors.ErrEntityNotFound.Wrap(dbErr)
//
// // Check error types
// if errors.Is(err, sdkErrors.ErrEntityNotFound) {
// // Handle not found error
// }
type SDKError struct {
// Code is the error code for programmatic error handling
Code ErrorCode
// Msg is the human-readable error message
Msg string
// Wrapped is the underlying error, if any
Wrapped error
}
// New creates a new SDKError with the specified error code, message, and
// optional wrapped error.
//
// Note: In most cases, you should use predefined errors
// (e.g., ErrEntityNotFound) and wrap them with .Wrap() instead of creating new
// errors from codes directly.
//
// Parameters:
// - code: the error code identifying the error type
// - msg: human-readable error message providing context
// - wrapped: optional underlying error to wrap (can be nil)
//
// Returns:
// - *SDKError: a new SDK error instance
//
// Example:
//
// // Creating a custom error (rare, prefer using predefined errors)
// err := sdkErrors.New(
// sdkErrors.ErrEntityNotFound.Code,
// "secret 'prod-api-key' not found in vault 'production'",
// dbErr,
// )
func New(code ErrorCode, msg string, wrapped error) *SDKError {
return &SDKError{
Code: code,
Msg: msg,
Wrapped: wrapped,
}
}
// Error implements the error interface, returning a formatted error message
// that includes the error code, message, and recursively includes wrapped
// error messages.
//
// Returns:
// - string: formatted error message with error code and full error chain
func (e *SDKError) Error() string {
if e.Wrapped != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Msg, e.Wrapped)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Msg)
}
// Unwrap returns the wrapped error, enabling error chain traversal with
// errors.Is() and errors.As() from the standard library.
//
// Returns:
// - error: the wrapped error, or nil if no error was wrapped
func (e *SDKError) Unwrap() error {
return e.Wrapped
}
// Wrap creates a new SDKError that wraps the provided error, preserving
// the current error's code and message while adding the new error to the
// error chain.
//
// Parameters:
// - err: the error to wrap in the error chain
//
// Returns:
// - *SDKError: a new SDK error with the same code and message but with
// the provided error wrapped
//
// Example:
//
// // Wrap a database error with entity not found error
// return sdkErrors.ErrEntityNotFound.Wrap(dbErr)
func (e *SDKError) Wrap(err error) *SDKError {
return &SDKError{
Code: e.Code,
Msg: e.Msg,
Wrapped: err,
}
}
// Is enables error comparison by error code using errors.Is() from the
// standard library. Two SDKErrors are considered equal if they have the
// same error code.
//
// Parameters:
// - target: the error to compare against
//
// Returns:
// - bool: true if target is an SDKError with the same error code
//
// Example:
//
// if errors.Is(err, sdkErrors.ErrEntityNotFound) {
// // Handle not found error
// }
func (e *SDKError) Is(target error) bool {
var t *SDKError
if errors.As(target, &t) {
return e.Code == t.Code
}
return false
}
// Clone creates a shallow copy of the SDKError. This is useful when you need
// to modify the Msg field of a sentinel error without mutating the original.
//
// Returns:
// - *SDKError: a new SDK error with the same code, message, and wrapped
// error as the original
//
// Example:
//
// // Copy a sentinel error to customize the message
// failErr := sdkErrors.ErrEntityNotFound.Copy()
// failErr.Msg = "secret 'prod-api-key' not found"
// return failErr
func (e *SDKError) Clone() *SDKError {
return &SDKError{
Code: e.Code,
Msg: e.Msg,
Wrapped: e.Wrapped,
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import (
"time"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// Delete marks secret versions as deleted for a given path. The deletion is
// performed by setting the DeletedTime to the current time.
//
// IMPORTANT: This is a soft delete. The path remains in the store even if all
// versions are deleted. To completely remove a path and reclaim memory, use
// Destroy() after deleting all versions.
//
// The function supports flexible version deletion with the following behavior:
// - If versions is empty, deletes only the current version
// - If versions contains specific numbers, deletes those versions
// - Version 0 in the array represents the current version
// - Non-existent versions are silently skipped without error
//
// This idempotent behavior is useful for batch operations where you want to
// ensure certain versions are deleted without failing if some don't exist.
//
// Parameters:
// - path: Path to the secret to delete
// - versions: Array of version numbers to delete (empty array deletes current
// version only, 0 in the array represents current version)
//
// Returns:
// - []int: Array of version numbers that were actually modified (had their
// DeletedTime changed from nil to now). Already-deleted versions are not
// included in this list.
// - *errors.SDKError: nil on success, or one of the following sdkErrors:
// - ErrEntityNotFound: if the path doesn't exist
//
// Example:
//
// // Delete current version only
// modified, err := kv.Delete("secret/path", []int{})
// if err != nil {
// log.Printf("Failed to delete secret: %v", err)
// }
// log.Printf("Deleted %d version(s): %v", len(modified), modified)
//
// // Delete specific versions
// modified, err = kv.Delete("secret/path", []int{1, 2, 3})
// if err != nil {
// log.Printf("Failed to delete versions: %v", err)
// }
// log.Printf("Actually deleted: %v", modified)
func (kv *KV) Delete(path string, versions []int) ([]int, *sdkErrors.SDKError) {
secret, exists := kv.data[path]
if !exists {
return nil, sdkErrors.ErrEntityNotFound
}
now := time.Now()
cv := secret.Metadata.CurrentVersion
var modified []int
// If no versions specified, mark the latest version as deleted
if len(versions) == 0 {
if v, exists := secret.Versions[cv]; exists && v.DeletedTime == nil {
v.DeletedTime = &now // Mark as deleted.
secret.Versions[cv] = v
modified = append(modified, cv)
}
return modified, nil
}
// Delete specific versions
for _, version := range versions {
if version == 0 {
v, exists := secret.Versions[cv]
if !exists || v.DeletedTime != nil {
continue
}
v.DeletedTime = &now // Mark as deleted.
secret.Versions[cv] = v
modified = append(modified, cv)
continue
}
if v, exists := secret.Versions[version]; exists && v.DeletedTime == nil {
v.DeletedTime = &now // Mark as deleted.
secret.Versions[version] = v
modified = append(modified, version)
}
}
return modified, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// Destroy permanently removes a secret path from the store, including all
// versions (both active and deleted). This is a hard delete operation that
// cannot be undone.
//
// Unlike Delete(), which soft-deletes versions by marking them with
// DeletedTime, Destroy() completely removes the path from the internal map,
// reclaiming the memory.
//
// This operation is useful for:
// - Purging secrets that have all versions deleted
// - Removing obsolete paths to prevent unbounded map growth
// - Compliance requirements for data removal
//
// Parameters:
// - path: The path to permanently remove from the store
//
// Returns:
// - *sdkErrors.SDKError: ErrEntityNotFound if the path does not exist,
// nil on success
//
// Example:
//
// // Delete all versions first
// kv.Delete("secret/path", []int{})
//
// // Check if empty and destroy
// secret, _ := kv.GetRawSecret("secret/path")
// if secret.IsEmpty() {
// err := kv.Destroy("secret/path")
// if err != nil {
// log.Printf("Failed to destroy secret: %v", err)
// }
// }
//
// // Or destroy directly (removes regardless of deletion state)
// err := kv.Destroy("secret/path")
func (kv *KV) Destroy(path string) *sdkErrors.SDKError {
if _, exists := kv.data[path]; !exists {
return sdkErrors.ErrEntityNotFound
}
delete(kv.data, path)
return nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import "time"
// Version represents a single version of versioned data along with its
// metadata. Each version maintains its own set of key-value pairs and tracking
// information.
type Version struct {
// Data contains the actual key-value pairs stored in this version
Data map[string]string
// CreatedTime is when this version was created
CreatedTime time.Time
// Version is the numeric identifier for this version. Version numbers
// start at 1 and increment with each update.
Version int
// DeletedTime indicates when this version was marked as deleted
// A nil value means the version is active/not deleted
DeletedTime *time.Time
}
// Metadata tracks control information for versioned data stored at a path.
// It maintains version boundaries and timestamps for the overall data
// collection.
type Metadata struct {
// CurrentVersion is the newest/latest non-deleted version number.
// Version numbers start at 1. A value of 0 indicates that all versions
// have been deleted (no valid version exists).
CurrentVersion int
// OldestVersion is the oldest available version number
OldestVersion int
// CreatedTime is when the data at this path was first created
CreatedTime time.Time
// UpdatedTime is when the data was last modified
UpdatedTime time.Time
// MaxVersions is the maximum number of versions to retain
// When exceeded, older versions are automatically pruned
MaxVersions int
}
// Value represents a versioned collection of key-value pairs stored at a
// specific path. It maintains both the version history and metadata about the
// collection as a whole.
type Value struct {
// Versions maps version numbers to their corresponding Version objects
Versions map[int]Version
// Metadata contains control information about this versioned data
Metadata Metadata
}
// HasValidVersions returns true if the Value has at least one non-deleted
// version. It iterates through all versions to check their DeletedTime.
//
// Returns:
// - true if any version has DeletedTime == nil (active version exists)
// - false if all versions are deleted or no versions exist
//
// Note: This method performs a full scan of all versions. For stores where
// CurrentVersion is maintained correctly (like SPIKE Nexus), checking
// Metadata.CurrentVersion != 0 is more efficient.
func (v *Value) HasValidVersions() bool {
for _, version := range v.Versions {
if version.DeletedTime == nil {
return true
}
}
return false
}
// Empty returns true if the Value has no valid (non-deleted) versions.
// This is the inverse of HasValidVersions() and is useful for identifying
// secrets that can be purged from storage.
//
// Returns:
// - true if all versions are deleted or no versions exist
// - false if at least one active version exists
//
// Example:
//
// if secret.IsEmpty() {
// // Safe to remove from storage
// kv.Destroy(path)
// }
func (v *Value) Empty() bool {
return !v.HasValidVersions()
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import sdkErrors "github.com/spiffe/spike-sdk-go/errors"
// Get retrieves a versioned key-value data map from the store at the specified
// path.
//
// The function supports versioned data retrieval with the following behavior:
// - If version is 0, returns the current version of the data
// - If version is specified, returns that specific version if it exists
// - Returns nil if the path doesn't exist
// - Returns nil if the specified version doesn't exist
// - Returns nil if the version has been deleted (DeletedTime is set)
//
// Parameters:
// - path: The path to retrieve data from
// - version: The specific version to retrieve (0 for current version)
//
// Returns:
// - map[string]string: The key-value data at the specified path and version,
// nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrEntityNotFound: if the path doesn't exist
// - ErrEntityDeleted: if the version doesn't exist or has been deleted
//
// Example:
//
// // Get current version
// data, err := kv.Get("secret/myapp", 0)
// if err != nil {
// log.Printf("Failed to get secret: %v", err)
// return
// }
//
// // Get specific version
// historicalData, err := kv.Get("secret/myapp", 2)
// if err != nil {
// log.Printf("Failed to get version 2: %v", err)
// return
// }
func (kv *KV) Get(path string, version int) (map[string]string, *sdkErrors.SDKError) {
secret, exists := kv.data[path]
if !exists {
return nil, sdkErrors.ErrEntityNotFound
}
// If the version not specified, use the current version:
if version == 0 {
version = secret.Metadata.CurrentVersion
}
v, exists := secret.Versions[version]
if !exists || v.DeletedTime != nil {
return nil, sdkErrors.ErrEntityDeleted
}
return v.Data, nil
}
// GetRawSecret retrieves a raw secret from the store at the specified path.
// This function is similar to Get, but it returns the raw Value object instead
// of the key-value data map, providing access to all versions and metadata.
//
// Parameters:
// - path: The path to retrieve the secret from
//
// Returns:
// - *Value: The complete secret object with all versions and metadata, nil
// on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrEntityNotFound: if the path doesn't exist
//
// Example:
//
// secret, err := kv.GetRawSecret("secret/myapp")
// if err != nil {
// log.Printf("Failed to get raw secret: %v", err)
// return
// }
// log.Printf("Current version: %d", secret.Metadata.CurrentVersion)
func (kv *KV) GetRawSecret(path string) (*Value, *sdkErrors.SDKError) {
secret, exists := kv.data[path]
if !exists {
return nil, sdkErrors.ErrEntityNotFound
}
return secret, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// ImportSecrets hydrates the key-value store with secrets loaded from
// persistent storage or a similar medium. It takes a map of path to secret
// values and adds them to the in-memory store. This is typically used during
// initialization or recovery after a system crash.
//
// The method performs a deep copy of all imported secrets to avoid sharing
// memory between the source data and the KV store. If a secret already exists
// in the store, it will be overwritten with the imported value. All version
// history and metadata from the imported secrets are preserved.
//
// Parameters:
// - secrets: Map of secret paths to their complete Value objects (including
// all versions and metadata)
//
// Returns:
// - None
//
// Example:
//
// secrets, err := persistentStore.LoadAllSecrets(context.Background())
// if err != nil {
// log.Fatalf("Failed to load secrets: %v", err)
// }
// kv.ImportSecrets(secrets)
// log.Printf("Imported %d secrets", len(secrets))
func (kv *KV) ImportSecrets(secrets map[string]*Value) {
for path, secret := range secrets {
// Create a deep copy of the secret to avoid sharing memory
newSecret := &Value{
Versions: make(map[int]Version, len(secret.Versions)),
Metadata: Metadata{
CreatedTime: secret.Metadata.CreatedTime,
UpdatedTime: secret.Metadata.UpdatedTime,
MaxVersions: kv.maxSecretVersions, // Use the KV store's setting
CurrentVersion: secret.Metadata.CurrentVersion,
OldestVersion: secret.Metadata.OldestVersion,
},
}
// Copy all versions
for versionNum, version := range secret.Versions {
// Deep copy the data map
dataCopy := make(map[string]string, len(version.Data))
for k, v := range version.Data {
dataCopy[k] = v
}
// Create the version copy
versionCopy := Version{
Data: dataCopy,
CreatedTime: version.CreatedTime,
Version: versionNum,
}
// Copy deleted time if set
if version.DeletedTime != nil {
deletedTime := *version.DeletedTime
versionCopy.DeletedTime = &deletedTime
}
newSecret.Versions[versionNum] = versionCopy
}
// Store the copied secret
kv.data[path] = newSecret
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
// Package kv provides an in-memory key-value store with automatic versioning
// and bounded cache semantics.
//
// # Concurrency Safety
//
// This package is NOT safe for concurrent use. All methods on KV must be
// externally synchronized. Callers are responsible for providing appropriate
// locking mechanisms (e.g., sync.RWMutex) to protect concurrent access.
//
// Concurrent operations without synchronization will cause data races and
// undefined behavior.
//
// Example of safe concurrent usage:
//
// type SafeStore struct {
// kv *kv.KV
// mu sync.RWMutex
// }
//
// func (s *SafeStore) Put(path string, data map[string]string) {
// s.mu.Lock()
// defer s.mu.Unlock()
// s.kv.Put(path, data)
// }
//
// func (s *SafeStore) Get(path string, version int) (map[string]string, error) {
// s.mu.RLock()
// defer s.mu.RUnlock()
// return s.kv.Get(path, version)
// }
//
// Use sync.RWMutex to allow concurrent reads while serializing writes for
// optimal performance.
package kv
// KV represents an in-memory key-value store with automatic versioning and
// bounded cache semantics. Each path maintains a configurable maximum number
// of versions, with older versions automatically pruned when the limit is
// exceeded.
//
// The store supports:
// - Versioned storage with automatic version numbering
// - Soft deletion with undelete capability
// - Bounded cache with automatic pruning of old versions
// - Version-specific retrieval and metadata tracking
type KV struct {
maxSecretVersions int
data map[string]*Value
}
// Config represents the configuration for a KV instance.
type Config struct {
// MaxSecretVersions is the maximum number of versions to retain per path.
// When exceeded, older versions are automatically pruned.
// Must be positive. A typical value is 10.
MaxSecretVersions int
}
// New creates a new KV instance with the specified configuration.
//
// The store is initialized as an empty in-memory key-value store with
// versioning enabled. All paths stored in this instance will retain up to
// MaxSecretVersions versions.
//
// Parameters:
// - config: Configuration specifying MaxSecretVersions
//
// Returns:
// - *KV: A new KV instance ready for use
//
// Example:
//
// kv := New(Config{MaxSecretVersions: 10})
// kv.Put("app/config", map[string]string{"key": "value"})
func New(config Config) *KV {
return &KV{
maxSecretVersions: config.MaxSecretVersions,
data: make(map[string]*Value),
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
// List returns a slice containing all paths stored in the key-value store.
// The order of paths in the returned slice is not guaranteed to be stable
// between calls.
//
// Note: List returns all paths regardless of whether their versions have been
// deleted. A path is only removed from the store when all of its data is
// explicitly removed, not when versions are soft-deleted.
//
// Returns:
// - []string: A slice containing all paths present in the store
//
// Example:
//
// kv := New(Config{MaxSecretVersions: 10})
// kv.Put("app/config", map[string]string{"key": "value"})
// kv.Put("app/database", map[string]string{"host": "localhost"})
//
// paths := kv.List()
// // Returns: ["app/config", "app/database"] (order not guaranteed)
func (kv *KV) List() []string {
keys := make([]string, 0, len(kv.data))
for k := range kv.data {
keys = append(keys, k)
}
return keys
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import (
"time"
)
// Put stores a new version of key-value pairs at the specified path in the
// store. It implements automatic versioning as a bounded cache with a
// configurable maximum number of versions per path.
//
// When storing values:
// - If the path doesn't exist, it creates new data with initial metadata
// - Each put operation creates a new version with an incremented version
// number
// - Old versions are automatically pruned when they fall outside the version
// window (CurrentVersion - MaxVersions)
// - All versions exceeding MaxVersions are pruned in a single operation,
// maintaining the most recent MaxVersions versions
// - Timestamps are updated for both creation and modification times
//
// Version pruning behavior (bounded cache):
// - Pruning occurs on each Put when versions exceed MaxVersions
// - All versions older than (CurrentVersion - MaxVersions) are deleted
// - Example: If CurrentVersion=15 and MaxVersions=10, versions 1-5 are
// deleted, keeping versions 6-15
// - This ensures O(n) pruning where n is the number of excess versions,
// providing predictable performance
//
// Parameters:
// - path: The location where the data will be stored
// - values: A map of key-value pairs to store at this path
//
// Example:
//
// kv := New(Config{MaxSecretVersions: 10})
// kv.Put("app/config", map[string]string{
// "api_key": "secret123",
// "timeout": "30s",
// })
// // Creates version 1 at path "app/config"
//
// kv.Put("app/config", map[string]string{
// "api_key": "newsecret456",
// "timeout": "60s",
// })
// // Creates version 2, version 1 is still available
//
// The function maintains metadata including:
// - CreatedTime: When the data at this path was first created
// - UpdatedTime: When the most recent version was added
// - CurrentVersion: The latest version number
// - OldestVersion: The oldest available version number after pruning
// - MaxVersions: Maximum number of versions to keep (configurable at KV
// creation)
func (kv *KV) Put(path string, values map[string]string) {
rightNow := time.Now()
secret, exists := kv.data[path]
if !exists {
secret = &Value{
Versions: make(map[int]Version),
Metadata: Metadata{
CreatedTime: rightNow,
UpdatedTime: rightNow,
MaxVersions: kv.maxSecretVersions,
// Versions start at 1, so that passing 0 as the version will
// default to the current version.
CurrentVersion: 1,
OldestVersion: 1,
},
}
kv.data[path] = secret
} else {
secret.Metadata.CurrentVersion++
}
newVersion := secret.Metadata.CurrentVersion
// Add a new version:
secret.Versions[newVersion] = Version{
Data: values,
CreatedTime: rightNow,
Version: newVersion,
}
// Update metadata
secret.Metadata.UpdatedTime = rightNow
// Clean up the old versions if exceeding MaxVersions
var deletedAny bool
for version := range secret.Versions {
if newVersion-version >= secret.Metadata.MaxVersions {
delete(secret.Versions, version)
deletedAny = true
}
}
if deletedAny {
oldestVersion := secret.Metadata.CurrentVersion
for version := range secret.Versions {
if version < oldestVersion {
oldestVersion = version
}
}
secret.Metadata.OldestVersion = oldestVersion
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package kv
import sdkErrors "github.com/spiffe/spike-sdk-go/errors"
// Undelete restores previously deleted versions of a secret at the specified
// path. It sets the DeletedTime to nil for each specified version that exists.
//
// The function supports flexible version restoration with the following behavior:
// - If versions is empty, restores only the current version
// - If versions contains specific numbers, restores those versions
// - Version 0 in the array represents the current version
// - Non-existent versions are silently skipped without error
//
// This idempotent behavior is useful for batch operations where you want to
// ensure certain versions are restored without failing if some don't exist.
//
// Parameters:
// - path: The location of the secret in the store
// - versions: Array of version numbers to restore (empty array restores
// current version only, 0 in the array represents current version)
//
// Returns:
// - []int: Array of version numbers that were actually modified (had their
// DeletedTime changed from non-nil to nil). Already-restored versions are
// not included in this list.
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrEntityNotFound: if the path doesn't exist
//
// Example:
//
// // Restore current version only
// modified, err := kv.Undelete("secret/path", []int{})
// if err != nil {
// log.Printf("Failed to undelete secret: %v", err)
// }
// log.Printf("Restored %d version(s): %v", len(modified), modified)
//
// // Restore specific versions
// modified, err = kv.Undelete("secret/path", []int{1, 2, 3})
// if err != nil {
// log.Printf("Failed to undelete versions: %v", err)
// }
// log.Printf("Actually restored: %v", modified)
func (kv *KV) Undelete(path string, versions []int) ([]int, *sdkErrors.SDKError) {
secret, exists := kv.data[path]
if !exists {
return nil, sdkErrors.ErrEntityNotFound
}
cv := secret.Metadata.CurrentVersion
var modified []int
// If no versions specified, mark the latest version as undeleted
if len(versions) == 0 {
if v, exists := secret.Versions[cv]; exists && v.DeletedTime != nil {
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[cv] = v
modified = append(modified, cv)
}
return modified, nil
}
// Undelete specific versions
for _, version := range versions {
if version == 0 {
v, exists := secret.Versions[cv]
if !exists || v.DeletedTime == nil {
continue
}
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[cv] = v
modified = append(modified, cv)
continue
}
if v, exists := secret.Versions[version]; exists && v.DeletedTime != nil {
v.DeletedTime = nil // Mark as undeleted.
secret.Versions[version] = v
modified = append(modified, version)
}
}
return modified, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package log
import (
"fmt"
"os"
"strings"
)
// Cannot import "env" here because of circular dependency.
const stackTracesOnLogFatalEnvVar = "SPIKE_STACK_TRACES_ON_LOG_FATAL"
// stackTracesOnLogFatalVal checks if stack traces should be enabled for fatal
// log calls by reading the SPIKE_STACK_TRACES_ON_LOG_FATAL environment
// variable.
//
// Returns:
// - bool: true if the environment variable is set to "true"
// (case-insensitive),
// false otherwise or if the variable is empty/unset
func stackTracesOnLogFatalVal() bool {
s := os.Getenv(stackTracesOnLogFatalEnvVar)
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return false
}
return s == "true"
}
// fatalExit terminates the program with exit code 1, or panics with a stack
// trace if SPIKE_STACK_TRACES_ON_LOG_FATAL is enabled. This provides a way
// to get detailed stack traces for debugging during development while using
// clean exits in production.
//
// Parameters:
// - fName: the name of the calling function for stack trace identification
// - args: variadic arguments to include in the panic message if stack traces
// are enabled
func fatalExit(fName string, args []any) {
if stackTracesOnLogFatalVal() {
ss := make([]string, len(args))
for i, arg := range args {
ss[i] = fmt.Sprint(arg)
}
panic(fName + " " + strings.Join(ss, ","))
}
os.Exit(1)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
// Package log provides a lightweight thread-safe logging facility
// using structured logging (slog) with JSON output format. It offers a
// singleton logger instance with configurable log levels through environment
// variables and convenience methods for fatal error logging.
package log
import (
"log/slog"
"os"
"strings"
"sync"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
var logger *slog.Logger
var loggerMutex sync.Mutex
// Log returns a thread-safe singleton instance of slog.Logger configured for
// JSON output. If the logger hasn't been initialized, it creates a new instance
// with the log level specified by the environment. Further calls return the
// same logger instance.
//
// By convention, when using the returned logger, the first argument (msg)
// should be the function name (fName) from which the logging call is made.
//
// Returns:
// - *slog.Logger: A thread-safe singleton logger instance
func Log() *slog.Logger {
loggerMutex.Lock()
defer loggerMutex.Unlock()
if logger != nil {
return logger
}
opts := &slog.HandlerOptions{
Level: Level(),
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger = slog.New(handler)
return logger
}
// Debug logs a message at Debug level.
//
// Parameters:
// - msg: The function name from which the call is made
// - args: Key-value pairs to be logged as structured fields
func Debug(msg string, args ...any) {
Log().Debug(msg, args...)
}
// Info logs a message at Info level.
//
// Parameters:
// - msg: The function name from which the call is made
// - args: Key-value pairs to be logged as structured fields
func Info(msg string, args ...any) {
Log().Info(msg, args...)
}
// Warn logs a message at Warn level.
//
// Parameters:
// - msg: The function name from which the call is made
// - args: Key-value pairs to be logged as structured fields
func Warn(msg string, args ...any) {
Log().Warn(msg, args...)
}
// Error logs a message at Error level.
//
// Parameters:
// - msg: The function name from which the call is made
// - args: Key-value pairs to be logged as structured fields
func Error(msg string, args ...any) {
Log().Error(msg, args...)
}
// FatalLn logs a message at Fatal level with a line feed.
//
// By default, this function exits cleanly with status code 1 to avoid leaking
// sensitive information through stack traces in production. To enable stack
// traces for development and testing, set SPIKE_STACK_TRACES_ON_LOG_FATAL=true.
//
// Parameters:
// - fName: The function name from which the call is made
// - args: The values to be logged, which will be formatted and joined
func FatalLn(fName string, args ...any) {
Log().Error(fName, args...)
fatalExit(fName, args)
}
// DebugErr logs an SDK error at Debug level.
//
// Parameters:
// - fName: The function name from which the call is made
// - err: An SDKError that will be logged with its message, code, and error
// text as structured fields
func DebugErr(fName string, err sdkErrors.SDKError) {
Log().Debug(
fName,
"message", err.Msg,
"code", err.Code,
"err", err.Error(),
)
}
// InfoErr logs an SDK error at Info level.
//
// Parameters:
// - fName: The function name from which the call is made
// - err: An SDKError that will be logged with its message, code, and error
// text as structured fields
func InfoErr(fName string, err sdkErrors.SDKError) {
Log().Info(
fName,
"message", err.Msg,
"code", err.Code,
"err", err.Error(),
)
}
// WarnErr logs an SDK error at Warn level.
//
// Parameters:
// - fName: The function name from which the call is made
// - err: An SDKError that will be logged with its message, code, and error
// text as structured fields
func WarnErr(fName string, err sdkErrors.SDKError) {
Log().Warn(
fName,
"message", err.Msg,
"code", err.Code,
"err", err.Error(),
)
}
// ErrorErr logs an SDK error at Error level.
//
// Parameters:
// - fName: The function name from which the call is made
// - err: An SDKError that will be logged with its message, code, and error
// text as structured fields
func ErrorErr(fName string, err sdkErrors.SDKError) {
Log().Error(
fName,
"message", err.Msg,
"code", err.Code,
"err", err.Error(),
)
}
// FatalErr logs an SDK error at Fatal level and exits the program.
//
// By default, this function exits cleanly with status code 1 to avoid leaking
// sensitive information through stack traces in production. To enable stack
// traces for development and testing, set SPIKE_STACK_TRACES_ON_LOG_FATAL=true.
//
// Parameters:
// - fName: The function name from which the call is made
// - err: An SDKError that will be logged with its message, code, and error
// text as structured fields
func FatalErr(fName string, err sdkErrors.SDKError) {
FatalLn(
fName,
"message", err.Msg,
"code", err.Code,
"err", err.Error(),
)
}
// Cannot get from env.go because of circular dependency.
const systemLogLevelEnvVar = "SPIKE_SYSTEM_LOG_LEVEL"
// Level returns the logging level for the SPIKE components.
//
// It reads from the SPIKE_SYSTEM_LOG_LEVEL environment variable and
// converts it to the corresponding slog.Level value.
//
// Returns:
// - slog.Level: The configured log level. Valid values (case-insensitive) are:
// - "DEBUG": returns slog.LevelDebug
// - "INFO": returns slog.LevelInfo
// - "WARN": returns slog.LevelWarn (default)
// - "ERROR": returns slog.LevelError
//
// If the environment variable is not set or contains an invalid value,
// it returns the default level slog.LevelWarn.
func Level() slog.Level {
level := os.Getenv(systemLogLevelEnvVar)
level = strings.ToUpper(level)
switch level {
case "DEBUG":
return slog.LevelDebug // -4
case "INFO":
return slog.LevelInfo // 0
case "WARN":
return slog.LevelWarn // 4
case "ERROR":
return slog.LevelError // 8
default:
return slog.LevelWarn // 4
}
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package net
import (
"context"
"fmt"
"io"
"net"
"net/http"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/config/env"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/predicate"
"github.com/spiffe/spike-sdk-go/spiffe"
)
// RequestBody reads and returns the entire request body as a byte slice.
// It reads all data from r.Body and ensures the body is properly closed
// after reading, even if an error occurs during the read operation.
//
// Close errors are logged but not returned to the caller, as the primary
// operation (reading the body data) has already completed. If reading fails,
// the error is returned immediately.
//
// Parameters:
// - r: HTTP request containing the body to read
//
// Returns:
// - bod: byte slice containing the full request body data on success, nil on
// error
// - err: *sdkErrors.SDKError with ErrNetReadingRequestBody if reading fails,
// nil on success (close errors are only logged)
//
// Example:
//
// body, err := RequestBody(req)
// if err != nil {
// log.Printf("Failed to read request body: %v", err)
// return
// }
// // Process body data...
func RequestBody(r *http.Request) (bod []byte, err *sdkErrors.SDKError) {
const fName = "RequestBody"
body, e := io.ReadAll(r.Body)
if e != nil {
failErr := sdkErrors.ErrNetReadingRequestBody.Wrap(e)
return nil, failErr
}
defer func(b io.ReadCloser) {
if b == nil {
return
}
failErr := sdkErrors.ErrFSStreamCloseFailed
log.WarnErr(fName, *failErr)
}(r.Body)
return body, err
}
// AuthorizerWithPredicate creates a TLS authorizer that validates SPIFFE IDs
// using the provided predicate function.
//
// The authorizer checks each connecting peer's SPIFFE ID against the predicate.
// If the predicate returns true, the connection is authorized. If false, the
// connection is rejected with ErrAccessUnauthorized.
//
// Parameters:
// - predicate: Function that takes a SPIFFE ID string and returns true to
// allow the connection, false to reject it
//
// Returns:
// - tlsconfig.Authorizer: A TLS authorizer that can be used with mTLS configs
//
// Example:
//
// // Allow only production namespace
// authorizer := AuthorizerWithPredicate(func(id string) bool {
// return strings.Contains(id, "/ns/production/")
// })
func AuthorizerWithPredicate(predicate func(string) bool) tlsconfig.Authorizer {
return tlsconfig.AdaptMatcher(func(id spiffeid.ID) error {
if predicate(id.String()) {
return nil
}
failErr := sdkErrors.ErrAccessUnauthorized
failErr.Msg = fmt.Sprintf("unauthorized spiffe id: '%s'", id.String())
return failErr
})
}
// CreateMTLSServerWithPredicate creates an HTTP server configured for mutual
// TLS (mTLS) authentication using SPIFFE X.509 certificates. It sets up the
// server with a custom authorizer that validates client SPIFFE IDs against a
// provided predicate function.
//
// Parameters:
// - source: An X509Source that provides the server's identity credentials and
// validates client certificates. Must not be nil.
// - tlsPort: The network address and port for the server to listen on
// (e.g., ":8443").
// - predicate: A function that takes a client SPIFFE ID string and returns
// true if the client should be allowed access, false otherwise.
//
// Returns:
// - *http.Server: A configured HTTP server ready to be started with TLS
// enabled.
//
// The server uses the provided X509Source for both its own identity and for
// validating client certificates. Client connections are only accepted if their
// SPIFFE ID passes the provided predicate function.
//
// Note: Terminates the program via log.FatalErr if source is nil, as this
// indicates a critical configuration error that should be caught during development.
func CreateMTLSServerWithPredicate(source *workloadapi.X509Source,
tlsPort string,
predicate func(string) bool) *http.Server {
const fName = "CreateMTLSServerWithPredicate"
if source == nil {
failErr := sdkErrors.ErrSPIFFENilX509Source
failErr.Msg = "source cannot be nil"
log.FatalErr(fName, *failErr)
}
authorizer := AuthorizerWithPredicate(predicate)
tlsConfig := tlsconfig.MTLSServerConfig(source, source, authorizer)
server := &http.Server{
Addr: tlsPort,
TLSConfig: tlsConfig,
ReadHeaderTimeout: env.HTTPServerReadHeaderTimeoutVal(),
// ^ Timeout for reading request headers,
// it helps prevent slowloris attacks
}
return server
}
// CreateMTLSServer creates an HTTP server configured for mutual TLS (mTLS)
// authentication using SPIFFE X.509 certificates.
//
// WARNING: This function accepts ALL client SPIFFE IDs without validation.
// For production use, consider using CreateMTLSServerWithPredicate to restrict
// which clients can connect to this server for better security.
//
// Parameters:
// - source: An X509Source that provides the server's identity credentials and
// validates client certificates. Must not be nil.
// - tlsPort: The network address and port for the server to listen on
// (e.g., ":8443").
//
// Returns:
// - *http.Server: A configured HTTP server ready to be started with TLS
// enabled.
//
// The server uses the provided X509Source for both its own identity and for
// validating client certificates. Client connections are accepted from ANY
// client with a valid SPIFFE certificate.
//
// Note: Terminates the program via log.FatalErr if source is nil, as this
// indicates a critical configuration error that should be caught during development.
func CreateMTLSServer(source *workloadapi.X509Source,
tlsPort string) *http.Server {
return CreateMTLSServerWithPredicate(source, tlsPort, predicate.AllowAll)
}
// CreateMTLSClientWithPredicate creates an HTTP client configured for
// mutual TLS authentication using SPIFFE workload identities.
//
// Parameters:
// - source: An X509Source that provides:
// - The client's own identity certificate (presented to servers)
// - Trusted roots for validating server certificates
// - predicate: A function that validates SERVER (peer) SPIFFE IDs.
// Returns true if the SERVER's ID should be trusted.
// NOTE: This predicate checks the SERVER's identity, NOT the client's.
//
// Returns:
// - *http.Client: A configured HTTP client that will use mTLS for all
// connections
//
// The returned client will:
// - Present its own client certificate from the X509Source to servers
// - Validate server certificates using the same X509Source's trust bundle
// - Only accept connections to servers whose SPIFFE IDs pass the predicate
//
// Example:
//
// // This predicate allows the client to connect only to servers with
// // SPIFFE IDs in the "backend" service namespace
// client := CreateMTLSClientWithPredicate(source,
// func(serverID string) bool {
// return strings.Contains(serverID, "/ns/backend/")
// })
func CreateMTLSClientWithPredicate(
source *workloadapi.X509Source,
predicate predicate.Predicate,
) *http.Client {
authorizer := AuthorizerWithPredicate(predicate)
tlsConfig := tlsconfig.MTLSClientConfig(source, source, authorizer)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
IdleConnTimeout: env.HTTPClientIdleConnTimeoutVal(),
MaxIdleConns: env.HTTPClientMaxIdleConnsVal(),
MaxConnsPerHost: env.HTTPClientMaxConnsPerHostVal(),
MaxIdleConnsPerHost: env.HTTPClientMaxIdleConnsPerHostVal(),
DialContext: (&net.Dialer{
Timeout: env.HTTPClientDialerTimeoutVal(),
KeepAlive: env.HTTPClientDialerKeepAliveVal(),
}).DialContext,
TLSHandshakeTimeout: env.HTTPClientTLSHandshakeTimeoutVal(),
ResponseHeaderTimeout: env.HTTPClientResponseHeaderTimeoutVal(),
ExpectContinueTimeout: env.HTTPClientExpectContinueTimeoutVal(),
},
Timeout: env.HTTPClientTimeoutVal(),
}
return client
}
// CreateMTLSClient creates an HTTP client configured for mutual TLS
// authentication using SPIFFE workload identities.
//
// WARNING: This function accepts ALL server SPIFFE IDs without validation.
// For production use, consider using CreateMTLSClientWithPredicate to restrict
// which servers this client will connect to for better security.
//
// Parameters:
// - source: An X509Source that provides the client's identity certificates
// and trusted roots
//
// Returns:
// - *http.Client: A configured HTTP client that will use mTLS for all
// connections
//
// The returned client will:
// - Present client certificates from the provided X509Source
// - Validate server certificates using the same X509Source
// - Accept connections to ANY server with a valid SPIFFE certificate
func CreateMTLSClient(source *workloadapi.X509Source) *http.Client {
return CreateMTLSClientWithPredicate(source, predicate.AllowAll)
}
// CreateMTLSClientForNexus creates an HTTP client configured for mutual TLS
// authentication with SPIKE Nexus using the provided X509Source. The client
// is configured with a predicate that validates peer IDs against the trusted
// Nexus root. Only peers that pass the spiffeid.IsNexus validation will be
// accepted for connections.
//
// Parameters:
// - source: An X509Source that provides the client's identity certificates
// and trusted roots
//
// Returns:
// - *http.Client: A configured HTTP client for connecting to SPIKE Nexus
func CreateMTLSClientForNexus(source *workloadapi.X509Source) *http.Client {
return CreateMTLSClientWithPredicate(source, predicate.AllowNexus)
}
// CreateMTLSClientForKeeper creates an HTTP client configured for mutual
// TLS authentication using the provided X509Source. The client is configured
// with a predicate that validates peer IDs against the trusted keeper root.
// Only peers that pass the spiffeid.IsKeeper validation will be accepted for
// connections.
//
// Parameters:
// - source: An X509Source that provides the client's identity certificates
// and trusted roots
//
// Returns:
// - *http.Client: A configured HTTP client for connecting to SPIKE Keeper
func CreateMTLSClientForKeeper(source *workloadapi.X509Source) *http.Client {
return CreateMTLSClientWithPredicate(source, predicate.AllowKeeper)
}
// Source creates and returns a new SPIFFE X509Source for workload API
// communication. It establishes a connection to the SPIFFE workload API using
// the default endpoint socket with a configurable timeout to prevent indefinite
// blocking on socket issues.
//
// The timeout can be configured using the SPIKE_SPIFFE_SOURCE_TIMEOUT
// environment variable (default: 30s).
//
// The function will terminate the program with exit code 1 if the source
// creation fails or times out.
//
// Returns:
// - *workloadapi.X509Source: A new X509Source for SPIFFE workload API
// communication
func Source() *workloadapi.X509Source {
const fName = "Source"
ctx, cancel := context.WithTimeout(
context.Background(),
env.SPIFFESourceTimeoutVal(),
)
defer cancel()
source, _, err := spiffe.Source(ctx, spiffe.EndpointSocket())
if err != nil {
failErr := sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err)
log.FatalErr(fName, *failErr)
}
return source
}
// ServeWithPredicate initializes and starts an HTTPS server using mTLS
// authentication with SPIFFE X.509 certificates. It sets up the server routes
// using the provided initialization function and listens for incoming
// connections on the specified port.
//
// Parameters:
// - source: An X509Source that provides the server's identity credentials and
// validates client certificates. Must not be nil.
// - initializeRoutes: A function that sets up the HTTP route handlers for the
// server. This function is called before the server starts.
// - predicate: a predicate function to pass to CreateMTLSServer.
// - tlsPort: The network address and port for the server to listen to on
// (e.g., ":8443").
//
// Returns:
// - *sdkErrors.SDKError: Returns nil if the server starts successfully,
// otherwise returns one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrFSStreamOpenFailed: if the server fails to start or encounters an error
// while running
//
// The function uses empty strings for the certificate and key file parameters
// in ListenAndServeTLS as the certificates are provided by the X509Source. The
// server's mTLS configuration is determined by the CreateMTLSServer function.
func ServeWithPredicate(source *workloadapi.X509Source,
initializeRoutes func(),
predicate func(string) bool,
tlsPort string) *sdkErrors.SDKError {
if source == nil {
failErr := sdkErrors.ErrSPIFFENilX509Source
failErr.Msg = "got nil source while trying to serve"
return failErr
}
initializeRoutes()
server := CreateMTLSServerWithPredicate(source, tlsPort, predicate)
if err := server.ListenAndServeTLS("", ""); err != nil {
failErr := sdkErrors.ErrFSStreamOpenFailed.Wrap(err)
failErr.Msg = "failed to listen and serve"
return failErr
}
return nil
}
// Serve initializes and starts an HTTPS server using mTLS
// authentication with SPIFFE X.509 certificates. It sets up the server routes
// using the provided initialization function and listens for incoming
// connections on the specified port.
//
// WARNING: This function accepts ALL client SPIFFE IDs without validation.
// For production use, consider using ServeWithPredicate to restrict
// which clients can connect to this server for better security.
//
// Parameters:
// - source: An X509Source that provides the server's identity credentials and
// validates client certificates. Must not be nil.
// - initializeRoutes: A function that sets up the HTTP route handlers for the
// server. This function is called before the server starts.
// - tlsPort: The network address and port for the server to listen on
// (e.g., ":8443").
//
// Returns:
// - *sdkErrors.SDKError: Returns nil if the server starts successfully,
// otherwise returns one of the following errors:
// - ErrSPIFFENilX509Source: if source is nil
// - ErrFSStreamOpenFailed: if the server fails to start or encounters an error
// while running
//
// The function uses empty strings for the certificate and key file parameters
// in ListenAndServeTLS as the certificates are provided by the X509Source. The
// server's mTLS configuration is determined by the CreateMTLSServer function.
func Serve(
source *workloadapi.X509Source,
initializeRoutes func(),
tlsPort string) *sdkErrors.SDKError {
return ServeWithPredicate(
source, initializeRoutes,
predicate.AllowAll, tlsPort,
)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package net
import (
"bytes"
"io"
"net/http"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// Post performs an HTTP POST request with a JSON payload and returns the
// response body. It handles the common cases of connection errors, non-200
// status codes, and proper response body handling.
//
// Parameters:
// - client: An *http.Client used to make the request, typically
// configured with TLS settings.
// - path: The URL path to send the POST request to.
// - mr: A byte slice containing the marshaled JSON request body.
//
// Returns:
// - []byte: The response body if the request is successful
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrAPIBadRequest: if request creation fails or server returns 400
// - ErrNetPeerConnection: if connection to peer fails or unexpected status
// code
// - ErrAPINotFound: if server returns 404
// - ErrAccessUnauthorized: if server returns 401
// - ErrStateNotReady: if server returns 503
// - ErrNetReadingResponseBody: if reading response body fails
//
// The function ensures proper cleanup by always attempting to close the
// response body, even if an error occurs during reading.
//
// Example:
//
// client := &http.Client{}
// data := []byte(`{"key": "value"}`)
// response, err := Post(client, "https://api.example.com/endpoint", data)
// if err != nil {
// log.Fatalf("failed to post: %v", err)
// }
func Post(
client *http.Client, path string, mr []byte,
) ([]byte, *sdkErrors.SDKError) {
const fName = "Post"
// Create the request while preserving the mTLS client
req, err := http.NewRequest("POST", path, bytes.NewBuffer(mr))
if err != nil {
failErr := sdkErrors.ErrAPIBadRequest.Wrap(err)
failErr.Msg = "failed to create request"
return nil, failErr
}
// Set headers
req.Header.Set("Content-Type", "application/json")
// Use the existing mTLS client to make the request
//nolint:bodyclose // Response body is properly closed in defer block
r, err := client.Do(req)
if err != nil {
failErr := sdkErrors.ErrNetPeerConnection.Wrap(err)
return []byte{}, failErr
}
defer func(b io.ReadCloser) {
if b == nil {
return
}
err := b.Close()
if err != nil {
failErr := sdkErrors.ErrFSStreamCloseFailed.Wrap(err)
failErr.Msg = "failed to close response body"
log.WarnErr(fName, *failErr)
}
}(r.Body)
if r.StatusCode != http.StatusOK {
if r.StatusCode == http.StatusNotFound {
return []byte{}, sdkErrors.ErrAPINotFound
}
if r.StatusCode == http.StatusUnauthorized {
return []byte{}, sdkErrors.ErrAccessUnauthorized
}
if r.StatusCode == http.StatusBadRequest {
return []byte{}, sdkErrors.ErrAPIBadRequest
}
// SPIKE Nexus is likely not initialized or in bad shape:
if r.StatusCode == http.StatusServiceUnavailable {
return []byte{}, sdkErrors.ErrStateNotReady
}
failErr := sdkErrors.ErrNetPeerConnection
failErr.Msg = "unexpected status code from peer"
return []byte{}, failErr
}
b, sdkErr := body(r)
if sdkErr != nil {
return nil, sdkErr
}
return b, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package net
import (
"encoding/json"
"github.com/spiffe/go-spiffe/v2/workloadapi"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// ResponseWithError is an interface for response types that include an error
// code field.
// This allows generic error handling across different API response types.
type ResponseWithError interface {
ErrorCode() sdkErrors.ErrorCode
}
// PostAndUnmarshal performs a complete request/response cycle for SPIKE Nexus
// API calls. It handles client creation, request posting, response
// unmarshaling, and error checking.
//
// Type parameter T must be a response type that implements ResponseWithError.
//
// Parameters:
// - source: X509Source for establishing mTLS connection to SPIKE Nexus
// - urlPath: The URL path to send the POST request to
// - requestBody: Marshaled JSON request body
//
// Returns:
// - (*T, nil) containing the unmarshaled response if successful
// - (nil, *sdkErrors.SDKError) if an error occurs:
// - Errors from Post(): including ErrAPINotFound, ErrAccessUnauthorized, etc.
// - ErrDataUnmarshalFailure: if response parsing fails
// - Error from FromCode(): if the response contains an error code
//
// Note: Callers should check for specific errors and handle them as needed:
//
// response, err := net.PostAndUnmarshal[MyResponse](source, url, body)
// if err != nil {
// if err.Is(sdkErrors.ErrAPINotFound) {
// // Handle not found case (e.g., return empty slice for lists)
// return &[]MyType{}, nil
// }
// return nil, err
// }
//
// Example:
//
// type MyResponse struct {
// Data string `json:"data"`
// Err sdkErrors.ErrorCode `json:"err,omitempty"`
// }
//
// func (r *MyResponse) ErrorCode() sdkErrors.ErrorCode { return r.Err }
//
// response, err := net.PostAndUnmarshal[MyResponse](
// source, "https://api.example.com/endpoint", requestBody)
func PostAndUnmarshal[T ResponseWithError](
source *workloadapi.X509Source,
urlPath string,
requestBody []byte,
) (*T, *sdkErrors.SDKError) {
client := CreateMTLSClientForNexus(source)
body, err := Post(client, urlPath, requestBody)
if err != nil {
return nil, err
}
var response T
if unmarshalErr := json.Unmarshal(body, &response); unmarshalErr != nil {
failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(unmarshalErr)
failErr.Msg = "problem parsing response body"
return nil, failErr
}
if errCode := response.ErrorCode(); errCode != "" {
return nil, sdkErrors.FromCode(errCode)
}
return &response, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package net
import (
"io"
"net/http"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// body reads and returns the entire response body from an HTTP response.
// The response body is read completely and returned as a byte slice.
//
// This is an internal helper function used by the net package to process
// HTTP responses.
//
// Parameters:
// - r: The HTTP response to read from
//
// Returns:
// - []byte: The complete response body, nil on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrNetReadingResponseBody: if reading the response body fails
//
// Example:
//
// resp, err := http.Get(url)
// if err != nil {
// return nil, err
// }
// defer resp.Body.Close()
//
// bodyBytes, sdkErr := body(resp)
// if sdkErr != nil {
// log.Printf("Failed to read response body: %v", sdkErr)
// return nil, sdkErr
// }
func body(r *http.Response) ([]byte, *sdkErrors.SDKError) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
failErr := sdkErrors.ErrNetReadingResponseBody.Wrap(err)
failErr.Msg = "failed to read HTTP response body"
return nil, failErr
}
return bodyBytes, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\ SPDX-License-Identifier: Apache-2.0
package net
import (
"io"
"net/http"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// StreamPostWithContentType performs an HTTP POST request with streaming data
// and a custom content type, returning the response body as a stream.
//
// This function is designed for streaming large amounts of data without loading
// the entire payload into memory.
//
// Resource Management: On success, returns an open io.ReadCloser that the caller
// MUST close (typically with defer). On error, any response body is automatically
// closed by this function and nil is returned, following the canonical Go pattern
// of returning (zero-value, error) on failures.
//
// Parameters:
// - client *http.Client: The HTTP client to use for the request
// - path string: The URL path to POST to
// - body io.Reader: The request body data stream
// - contentType ContentType: The MIME type of the request body
// (e.g., ContentTypeJSON, ContentTypeTextPlain, ContentTypeOctetStream)
//
// Returns:
// - io.ReadCloser: The response body stream on success (must be closed by caller),
// nil on error (already closed by this function)
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrAPINotFound (404): Resource not found
// - ErrAccessUnauthorized (401): Authentication required
// - ErrAPIBadRequest (400): Invalid request
// - ErrStateNotReady (503): Service unavailable
// - Generic error for other non-200 status codes
//
// Example:
//
// data := strings.NewReader("large data payload")
// response, err := StreamPostWithContentType(client,
// "/api/upload", data, "text/plain")
// if err != nil {
// return err
// }
// defer response.Close()
// // Process streaming response...
func StreamPostWithContentType(
client *http.Client, path string, body io.Reader,
contentType ContentType,
) (io.ReadCloser, *sdkErrors.SDKError) {
const fName = "StreamPostWithContentType"
req, err := http.NewRequest("POST", path, body)
if err != nil {
failErr := sdkErrors.ErrAPIPostFailed.Wrap(err)
failErr.Msg = "failed to create request"
return nil, failErr
}
req.Header.Set("Content-Type", string(contentType))
r, err := client.Do(req)
if err != nil {
failErr := sdkErrors.ErrNetPeerConnection.Wrap(err)
return nil, failErr
}
if r.StatusCode != http.StatusOK {
// Close body on error paths before returning
if r.Body != nil {
closeErr := r.Body.Close()
if closeErr != nil {
failErr := sdkErrors.ErrFSStreamCloseFailed
failErr.Msg = "failed to close response body on error path"
log.WarnErr(fName, *failErr)
}
}
switch r.StatusCode {
case http.StatusNotFound:
return nil, sdkErrors.ErrAPINotFound
case http.StatusUnauthorized:
return nil, sdkErrors.ErrAccessUnauthorized
case http.StatusBadRequest:
return nil, sdkErrors.ErrAPIBadRequest
case http.StatusServiceUnavailable:
return nil, sdkErrors.ErrStateNotReady
default:
failErr := sdkErrors.ErrNetPeerConnection
return nil, failErr
}
}
// Success: return open body for caller to close
return r.Body, nil
}
// StreamPost is a convenience wrapper for StreamPostWithContentType that uses
// the default content type ContentTypeOctetStream ("application/octet-stream").
//
// This function is ideal for posting binary data or when the specific content
// type doesn't matter. The caller is responsible for closing the returned
// io.ReadCloser.
//
// Parameters:
// - client *http.Client: The HTTP client to use for the request
// - path string: The URL path to POST to
// - body io.Reader: The request body data stream
//
// Returns:
// - io.ReadCloser: The response body stream if successful
// (must be closed by caller)
// - *sdkErrors.SDKError: nil on success, or a well-known error
// (see StreamPostWithContentType)
//
// Example:
//
// binaryData := bytes.NewReader(fileBytes)
// response, err := StreamPost(client, "/api/upload", binaryData)
// if err != nil {
// return err
// }
// defer response.Close()
// // Process response...
func StreamPost(
client *http.Client, path string, body io.Reader,
) (io.ReadCloser, *sdkErrors.SDKError) {
return StreamPostWithContentType(
client, path, body, ContentTypeOctetStream,
)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
// Package predicate provides SPIFFE ID validation predicates for SPIKE API
// access control.
//
// This package defines predicate functions that can be used to validate
// SPIFFE IDs in API calls, enabling fine-grained access control based on
// workload identity.
// Predicates are used by API methods to restrict access to specific types of
// workloads (e.g., only SPIKE Pilot instances).
package predicate
import (
"github.com/spiffe/spike-sdk-go/spiffeid"
)
// Predicate is a function type that validates a SPIFFE ID string.
// It returns true if the SPIFFE ID should be allowed access, false otherwise.
//
// Predicates are used throughout the SPIKE API to implement access control
// policies based on workload identity. They are typically passed to API methods
// to restrict which workloads can perform specific operations.
//
// Example usage:
//
// // Create a predicate that only allows pilot workloads
// pilotPredicate := AllowPilot("example.org")
//
// // Use in an API call
// policy, err := acl.GetPolicy(source, policyID, pilotPredicate)
type Predicate func(string) bool
// AllowAll is a predicate that accepts any SPIFFE ID.
// This effectively disables access control and should be used with caution.
// It's typically used when policy-based access control is handled at a
// higher level.
//
// Example usage:
//
// // Allow any workload to access the API
// secret, err := secret.Get(source, path, version, AllowAll)
var AllowAll = Predicate(func(_ string) bool { return true })
// DenyAll is a predicate that rejects all SPIFFE IDs.
// This can be used to temporarily disable access or as a default restrictive
// policy.
//
// Example usage:
//
// // Deny all access during maintenance
// policy, err := acl.GetPolicy(source, policyID, DenyAll)
var DenyAll = Predicate(func(_ string) bool { return false })
// AllowNexus is a predicate that only allows SPIKE Nexus workloads.
// It validates whether a given SPIFFE ID matches the SPIKE Nexus identity
// pattern for the configured trust domains.
//
// This is used to restrict API access to only SPIKE Nexus instances, providing
// an additional layer of security for sensitive operations that should only
// be performed by the data plane storage component.
//
// The predicate uses trust domains configured via environment variables.
//
// Example usage:
//
// // Use predicate for nexus-only access
// policy, err := acl.GetPolicy(source, policyID, AllowNexus)
// secret, err := secret.Get(source, secretPath, version, AllowNexus)
//
// The returned predicate will accept SPIFFE IDs matching:
// - "spiffe://example.org/spike/nexus"
// - "spiffe://example.org/spike/nexus/instance-1"
// - "spiffe://dev.example.org/spike/nexus"
// - etc.
//
// based on the trust domains configured in the environment.
var AllowNexus = Predicate(
func(SPIFFEID string) bool { return spiffeid.IsNexus(SPIFFEID) },
)
// AllowKeeper is a predicate that only allows SPIKE Keeper workloads.
// It validates whether a given SPIFFE ID matches the SPIKE Keeper identity
// pattern for the configured trust domains.
//
// This is used to restrict API access to only SPIKE Keeper instances, providing
// an additional layer of security for operations that should only be performed
// by the key management component.
//
// The predicate uses trust domains configured via environment variables.
//
// Example usage:
//
// // Use predicate for keeper-only access
// policy, err := acl.GetPolicy(source, policyID, AllowKeeper)
// secret, err := secret.Get(source, secretPath, version, AllowKeeper)
//
// The predicate will accept SPIFFE IDs matching:
// - "spiffe://example.org/spike/keeper"
// - "spiffe://example.org/spike/keeper/instance-1"
// - "spiffe://dev.example.org/spike/keeper"
// - etc.
//
// based on the trust domains configured in the environment.
var AllowKeeper = Predicate(
func(SPIFFEID string) bool { return spiffeid.IsKeeper(SPIFFEID) },
)
// AllowKeeperPeer is a predicate function that validates whether a peer
// SPIFFE ID is authorized to communicate with SPIKE Keeper instances.
//
// For security reasons, only SPIKE Nexus and SPIKE Bootstrap components
// are allowed to communicate with SPIKE Keeper. This function enforces
// this security policy by checking if the peer SPIFFE ID matches either
// the Nexus or Bootstrap identity patterns.
//
// Parameters:
// - peerSpiffeId: The SPIFFE ID string of the peer attempting to connect
//
// Returns:
// - bool: true if the peer is authorized (Nexus or Bootstrap),
// false otherwise
//
// Example usage:
//
// // Use in server configuration to restrict Keeper access
// if AllowKeeperPeer(clientSpiffeId) {
// // Allow connection to Keeper
// } else {
// // Deny connection
// }
//
// The function will return true for SPIFFE IDs matching:
// - SPIKE Nexus: "spiffe://example.org/spike/nexus"
// - SPIKE Bootstrap: "spiffe://example.org/spike/bootstrap"
// - Extended variants with additional path segments
var AllowKeeperPeer = func(peerSpiffeId string) bool {
// Security: Only SPIKE Nexus and SPIKE Bootstrap
// can talk to SPIKE Keepers.
return spiffeid.PeerCanTalkToKeeper(peerSpiffeId)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package mock
import (
"context"
)
// Retrier implements Retrier for testing
type Retrier struct {
RetryFunc func(context.Context, func() error) error
}
// RetryWithBackoff implements the Retrier interface
func (m *Retrier) RetryWithBackoff(
ctx context.Context,
operation func() error,
) error {
if m.RetryFunc != nil {
return m.RetryFunc(ctx, operation)
}
return nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
// Package retry provides a flexible and type-safe retry mechanism with
// exponential backoff. It allows for customizable retry strategies and
// notifications while maintaining context awareness and cancellation support.
package retry
import (
"context"
"errors"
"time"
"github.com/cenkalti/backoff/v4"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// Default configuration values for the exponential backoff strategy
const (
// Initial wait time between retries
defaultInitialInterval = 500 * time.Millisecond
// Maximum wait time between retries
defaultMaxInterval = 60 * time.Second
// Maximum total time for all retry attempts
defaultMaxElapsedTime = 1200 * time.Second
// A zero max elapsed time means try forever.
forever = 0
// Factor by which the wait time increases
defaultMultiplier = 2.0
)
// Retrier defines the interface for retry operations with backoff support.
// Implementations of this interface provide different retry strategies.
type Retrier interface {
// RetryWithBackoff executes an operation with a backoff strategy.
// It will repeatedly execute the operation until it succeeds or
// the context is canceled. The backoff strategy determines the
// delay between retry attempts.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - op: The operation to retry, returns error if the attempt failed
//
// Returns:
// - *sdkErrors.SDKError: nil if successful, or one of the following:
// - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
// - ErrRetryContextCanceled: if context is canceled
// - The last error from the operation
RetryWithBackoff(
ctx context.Context, op func() *sdkErrors.SDKError,
) *sdkErrors.SDKError
}
// TypedRetrier provides type-safe retry operations for functions that return
// both a value and an error. It wraps a base Retrier to provide typed results.
type TypedRetrier[T any] struct {
retrier Retrier
}
// NewTypedRetrier creates a new TypedRetrier with the given base Retrier.
// This allows for type-safe retry operations while reusing existing retry
// logic.
//
// Parameters:
// - r: The base Retrier implementation to wrap
//
// Returns:
// - *TypedRetrier[T]: A new TypedRetrier instance for the specified type
//
// Example:
//
// retrier := NewTypedRetrier[string](NewExponentialRetrier())
// result, err := retrier.RetryWithBackoff(ctx, func() (
// string, *sdkErrors.SDKError) {
// return callExternalService()
// })
func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] {
return &TypedRetrier[T]{retrier: r}
}
// RetryWithBackoff executes a typed operation with a backoff strategy.
// It preserves the return value while maintaining retry functionality.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - op: The operation to retry, returns both a value and an error
//
// Returns:
// - T: The result value from the successful operation
// - *sdkErrors.SDKError: nil if successful, or one of the following errors:
// - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
// - ErrRetryContextCanceled: if context is canceled
// - The wrapped error from the operation if it fails
func (r *TypedRetrier[T]) RetryWithBackoff(
ctx context.Context,
op func() (T, *sdkErrors.SDKError),
) (T, *sdkErrors.SDKError) {
var result T
err := r.retrier.RetryWithBackoff(ctx, func() *sdkErrors.SDKError {
var opErr *sdkErrors.SDKError
result, opErr = op()
return opErr
})
return result, err
}
// NotifyFn is a callback function type for retry notifications.
// It provides information about each retry attempt, including the error,
// current interval duration, and total elapsed time.
type NotifyFn func(
err *sdkErrors.SDKError, duration, totalDuration time.Duration,
)
// RetrierOption is a function type for configuring ExponentialRetrier.
// It follows the functional options pattern for flexible configuration.
type RetrierOption func(*ExponentialRetrier)
// ExponentialRetrier implements Retrier using exponential backoff strategy.
// It provides configurable retry intervals and maximum attempt durations.
type ExponentialRetrier struct {
newBackOff func() backoff.BackOff
notify NotifyFn
}
// BackOffOption is a function type for configuring ExponentialBackOff.
// It allows fine-tuning of the backoff strategy parameters.
type BackOffOption func(*backoff.ExponentialBackOff)
// NewExponentialRetrier creates a new ExponentialRetrier with configurable
// settings. Default values provide sensible backoff behavior for most use
// cases.
//
// Default settings:
// - InitialInterval: 500ms
// - MaxInterval: 60s
// - MaxElapsedTime: 1200s (20 minutes)
// - Multiplier: 2.0
//
// Parameters:
// - opts: Optional configuration functions to customize retry behavior
//
// Returns:
// - *ExponentialRetrier: A configured retrier instance ready for use
//
// Example:
//
// retrier := NewExponentialRetrier(
// WithBackOffOptions(
// WithInitialInterval(100 * time.Millisecond),
// WithMaxInterval(5 * time.Second),
// ),
// WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
// log.Printf("Retry attempt failed: %v", err)
// }),
// )
func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier {
b := backoff.NewExponentialBackOff()
b.InitialInterval = defaultInitialInterval
b.MaxInterval = defaultMaxInterval
b.MaxElapsedTime = defaultMaxElapsedTime
b.Multiplier = defaultMultiplier
r := &ExponentialRetrier{
newBackOff: func() backoff.BackOff {
return b
},
}
for _, opt := range opts {
opt(r)
}
return r
}
// RetryWithBackoff implements the Retrier interface using exponential backoff.
// It executes the operation repeatedly until success or context cancellation.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - operation: The function to retry that returns an error
//
// Returns:
// - *sdkErrors.SDKError: nil if the operation eventually succeeds, or one of:
// - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
// - ErrRetryContextCanceled: if context is canceled
// - The last error from the operation
func (r *ExponentialRetrier) RetryWithBackoff(
ctx context.Context,
operation func() *sdkErrors.SDKError,
) *sdkErrors.SDKError {
b := r.newBackOff()
totalDuration := time.Duration(0)
// Wrap operation to convert SDKError to plain error for backoff library
wrappedOp := func() error {
sdkErr := operation()
if sdkErr == nil {
return nil
}
return sdkErr
}
err := backoff.RetryNotify(
wrappedOp,
backoff.WithContext(b, ctx),
func(err error, duration time.Duration) {
totalDuration += duration
if r.notify != nil {
// Convert plain error back to SDKError for notification
var sdkErr *sdkErrors.SDKError
if errors.As(err, &sdkErr) {
r.notify(sdkErr, duration, totalDuration)
} else {
// Wrap plain error if it's not already an SDKError
wrapped := sdkErrors.ErrRetryOperationFailed.Wrap(err)
r.notify(wrapped, duration, totalDuration)
}
}
},
)
if err == nil {
return nil
}
// Check if error is already an SDKError
var sdkErr *sdkErrors.SDKError
if errors.As(err, &sdkErr) {
return sdkErr
}
// Wrap context errors appropriately
if errors.Is(err, context.Canceled) {
failErr := sdkErrors.ErrRetryContextCanceled.Wrap(err)
failErr.Msg = "retry operation canceled"
return failErr
}
if errors.Is(err, context.DeadlineExceeded) {
failErr := sdkErrors.ErrRetryMaxElapsedTimeReached.Wrap(err)
failErr.Msg = "maximum retry elapsed time exceeded"
return failErr
}
// Wrap any other error
failErr := sdkErrors.ErrRetryOperationFailed.Wrap(err)
failErr.Msg = "retry operation failed"
return failErr
}
// WithBackOffOptions configures the backoff settings using the provided
// options. Multiple options can be combined to customize the retry behavior.
//
// Parameters:
// - opts: One or more BackOffOption functions to configure the backoff
// strategy
//
// Returns:
// - RetrierOption: A configuration function for ExponentialRetrier
//
// Example:
//
// retrier := NewExponentialRetrier(
// WithBackOffOptions(
// WithInitialInterval(1 * time.Second),
// WithMaxElapsedTime(1 * time.Minute),
// ),
// )
func WithBackOffOptions(opts ...BackOffOption) RetrierOption {
return func(r *ExponentialRetrier) {
b := r.newBackOff().(*backoff.ExponentialBackOff)
for _, opt := range opts {
opt(b)
}
}
}
// WithInitialInterval sets the initial interval between retries.
// This is the starting point for the exponential backoff calculation.
//
// Parameters:
// - d: The initial wait duration before the first retry
//
// Returns:
// - BackOffOption: A configuration function for ExponentialBackOff
func WithInitialInterval(d time.Duration) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.InitialInterval = d
}
}
// WithMaxInterval sets the maximum interval between retries.
// The interval will never exceed this value, regardless of the multiplier.
//
// Parameters:
// - d: The maximum wait duration between retry attempts
//
// Returns:
// - BackOffOption: A configuration function for ExponentialBackOff
func WithMaxInterval(d time.Duration) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.MaxInterval = d
}
}
// WithMaxElapsedTime sets the maximum total time for retries.
// The retry operation will stop after this duration, even if not successful.
// Set to 0 to retry indefinitely (until context is canceled).
//
// Parameters:
// - d: The maximum total duration for all retry attempts
//
// Returns:
// - BackOffOption: A configuration function for ExponentialBackOff
func WithMaxElapsedTime(d time.Duration) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.MaxElapsedTime = d
}
}
// WithMultiplier sets the multiplier for increasing intervals.
// Each retry interval is multiplied by this value, up to MaxInterval.
//
// Parameters:
// - m: The multiplier factor (e.g., 2.0 doubles the interval each time)
//
// Returns:
// - BackOffOption: A configuration function for ExponentialBackOff
func WithMultiplier(m float64) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.Multiplier = m
}
}
// WithRandomizationFactor sets the randomization factor for backoff intervals.
// The actual interval will be randomized between
// [interval * (1 - factor), interval * (1 + factor)].
//
// A factor of 0 disables randomization (deterministic intervals).
// A factor of 0.5 (the default) means intervals can vary by ±50%.
// This randomization helps prevent thundering herd issues in distributed
// systems.
//
// Parameters:
// - factor: The randomization factor (0.0 to 1.0)
//
// Returns:
// - BackOffOption: A configuration function for ExponentialBackOff
func WithRandomizationFactor(factor float64) BackOffOption {
return func(b *backoff.ExponentialBackOff) {
b.RandomizationFactor = factor
}
}
// WithNotify is an option to set the notification callback.
// The callback is called after each failed attempt, allowing you to log
// or monitor retry behavior.
//
// Parameters:
// - fn: Callback function invoked after each failed retry attempt
//
// Returns:
// - RetrierOption: A configuration function for ExponentialRetrier
//
// Example:
//
// retrier := NewExponentialRetrier(
// WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
// log.Printf("Attempt failed after %v, total time %v: %v",
// d, total, err)
// }),
// )
func WithNotify(fn NotifyFn) RetrierOption {
return func(r *ExponentialRetrier) {
r.notify = fn
}
}
// Handler represents a function that returns a value and an error.
// It's used with the Do helper function for simple retry operations.
type Handler[T any] func() (T, *sdkErrors.SDKError)
// Do provides a simplified way to retry a typed operation with configurable
// settings. It creates a TypedRetrier with exponential backoff and applies
// any provided options.
//
// This is a convenience function for common retry scenarios where you don't
// need to create and manage a retrier instance explicitly.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - handler: The function to retry that returns a value and error
// - options: Optional configuration for the retry behavior
//
// Returns:
// - T: The result value from the successful operation
// - *sdkErrors.SDKError: nil if successful, or one of the following errors:
// - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
// - ErrRetryContextCanceled: if context is canceled
// - The wrapped error from the handler if it fails
//
// Example:
//
// result, err := Do(ctx, func() (string, *sdkErrors.SDKError) {
// return fetchData()
// }, WithNotify(logRetryAttempts))
func Do[T any](
ctx context.Context, handler Handler[T], options ...RetrierOption,
) (T, *sdkErrors.SDKError) {
return NewTypedRetrier[T](
NewExponentialRetrier(options...),
).RetryWithBackoff(ctx, handler)
}
// Forever retries an operation indefinitely with exponential backoff until it
// succeeds or the context is canceled. It sets MaxElapsedTime to 0, which means
// the retry loop will continue forever (or until the context is canceled).
//
// This is a convenience function that sets up exponential backoff with sensible
// defaults for infinite retry scenarios.
//
// Default settings:
// - InitialInterval: 500ms
// - MaxInterval: 60s
// - MaxElapsedTime: 0 (retry forever)
// - Multiplier: 2.0
//
// Parameters:
// - ctx: Context for cancellation control (the only way to stop retrying)
// - handler: The function to retry that returns a value and error
// - options: Optional configuration for retry behavior
//
// Note: User-provided options are applied AFTER the default settings and will
// override them. If you pass WithBackOffOptions(WithMaxElapsedTime(...)), it
// will override the "forever" behavior. This allows power users to customize
// the retry behavior while keeping the convenience of preset defaults.
//
// Returns:
// - T: The result value from the successful operation
// - *sdkErrors.SDKError: nil if successful, or one of the following errors:
// - ErrRetryContextCanceled: if context is canceled
// - The wrapped error from the handler if all retries fail
//
// Example:
//
// // Retry forever with custom notification
// result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) {
// return fetchData()
// }, WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
// log.Printf("Retry failed: %v (attempt duration: %v, total: %v)",
// err, d, total)
// }))
//
// // Override behavior (will now stop after 1 minute
// // instead of retrying forever)
// result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) {
// return fetchData()
// }, WithBackOffOptions(WithMaxElapsedTime(1 * time.Minute)))
func Forever[T any](
ctx context.Context, handler Handler[T], options ...RetrierOption,
) (T, *sdkErrors.SDKError) {
ro := WithBackOffOptions(WithMaxElapsedTime(forever))
ros := []RetrierOption{ro}
ros = append(ros, options...)
return NewTypedRetrier[T](
NewExponentialRetrier(ros...),
).RetryWithBackoff(ctx, handler)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
//go:build !windows
package mem
import (
"syscall"
"github.com/spiffe/spike-sdk-go/log"
)
// Lock attempts to lock the process memory to prevent swapping.
// Returns true if successful, false if not supported or failed.
func Lock() bool {
const fName = "Lock"
// Attempt to lock all current and future memory
if err := syscall.Mlockall(
syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil {
log.Log().Warn(fName, "msg", "Failed to lock memory", "err", err.Error())
return false
}
return true
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package mem
import (
"crypto/rand"
"runtime"
"unsafe"
"github.com/spiffe/spike-sdk-go/log"
)
// ClearRawBytes securely erases all bytes in the provided value by overwriting
// its memory with zeros. This ensures sensitive data like cryptographic keys
// and Shamir shards are properly cleaned from memory before garbage collection.
//
// According to NIST SP 800-88 Rev. 1 (Guidelines for Media Sanitization),
// a single overwrite pass with zeros is sufficient for modern storage
// devices, including RAM.
//
// IMPORTANT LIMITATIONS:
//
// This function only clears the direct memory occupied by the struct/value.
// It does NOT clear data referenced by pointers, slices, maps, or channels.
// For structs containing reference types, you must clear the referenced
// data separately before calling this function.
//
// Examples of what is NOT cleared:
// - Data pointed to by pointers within the struct
// - Underlying arrays of slices
// - Keys and values in maps
// - Immutable string data (only the string header is cleared)
// - Data in channels
//
// APPROPRIATE USE CASES:
// - Fixed-size byte arrays: [32]byte, [64]byte, etc.
// - Structs containing only value types (no pointers/slices/maps)
// - Primitive types: int, float64, bool, etc.
// - Arrays of primitive types
//
// INAPPROPRIATE USE CASES:
// - Structs with pointer fields (unless you only want to clear the pointers)
// - Slices, maps, channels, interfaces
// - Structs with embedded reference types
//
// For general struct clearing with proper Go semantics, consider:
//
// var zero T
// *s = zero
//
// Parameters:
// - s: A pointer to any type of data that should be securely erased
//
// Usage examples:
//
// // GOOD: Fixed-size byte array
// key := &[32]byte{...}
// defer ClearRawBytes(key)
//
// // GOOD: Struct with only value types
// type Coordinates struct {
// X, Y, Z float64
// Valid bool
// }
// coords := &Coordinates{...}
// defer ClearRawBytes(coords)
//
// // CAUTION: Struct with pointers - only clears the pointer values
// type MixedData struct {
// Key [32]byte // This will be cleared
// Secret *string // Only the pointer is cleared, not the string data
// Tokens []byte // Only slice header is cleared, not the underlying
// array
// }
// data := &MixedData{...}
// // Clear referenced data first:
// ClearRawBytes(data.Secret) // Clear the string (if it points to a fixed
// // array)
// ClearRawBytes(&data.Tokens[0]) // Clear slice data manually if needed
// ClearRawBytes(data) // Finally clear the struct itself.
func ClearRawBytes[T any](s *T) {
if s == nil {
return
}
p := unsafe.Pointer(s)
size := unsafe.Sizeof(*s)
b := (*[1 << 30]byte)(p)[:size:size]
// Zero out all bytes in mem
for i := range b {
b[i] = 0
}
// Make sure the data is actually wiped before gc has time to interfere
runtime.KeepAlive(s)
}
// ClearRawBytesParanoid provides a more thorough memory wiping method for
// highly sensitive data.
//
// It performs multiple passes using different patterns (zeros, ones,
// random data, and alternating bits) to minimize potential data remanence
// concerns from sophisticated physical memory attacks.
//
// This method is designed for extremely security-sensitive applications where:
// 1. An attacker might have physical access to RAM
// 2. Cold boot attacks or specialized memory forensics equipment might be
// used
// 3. The data being protected is critically sensitive (e.g., high-value
// encryption keys)
//
// For most applications, the standard ClearRawBytes() method is sufficient as:
// - Modern RAM technologies (DDR4/DDR5) make data remanence attacks
// increasingly difficult
// - Successful attacks typically require specialized equipment and immediate
// (sub-second) physical access.
// - The time window for such attacks is extremely short after power loss
// - The detectable signal from previous memory states diminishes rapidly with
// a single overwrite
//
// This method is provided for users with extreme security requirements or in
// regulated environments where multiple-pass overwrite policies are mandated.
func ClearRawBytesParanoid[T any](s *T) {
const fName = "ClearRawBytesParanoid"
if s == nil {
return
}
p := unsafe.Pointer(s)
size := unsafe.Sizeof(*s)
b := (*[1 << 30]byte)(p)[:size:size]
// Pattern overwrite cycles:
// 1. All zeros
// 2. All ones (0xFF)
// 3. Random data
// 4. Alternating 0x55/0xAA (01010101/10101010)
// 5. Final zero out
// Zero out all bytes (first pass)
for i := range b {
b[i] = 0
}
runtime.KeepAlive(s)
// Fill with ones (second pass)
for i := range b {
b[i] = 0xFF
}
runtime.KeepAlive(s)
// Fill with random data (third pass)
_, err := rand.Read(b)
if err != nil {
log.FatalLn(fName)
}
runtime.KeepAlive(s)
// Alternating bit pattern (fourth pass)
for i := range b {
if i%2 == 0 {
b[i] = 0x55 // 01010101
} else {
b[i] = 0xAA // 10101010
}
}
runtime.KeepAlive(s)
// Final zero out (fifth pass)
for i := range b {
b[i] = 0
}
runtime.KeepAlive(s)
}
// Zeroed32 checks if a 32-byte array contains only zero values.
// Returns true if all bytes are zero, false otherwise.
func Zeroed32(ar *[32]byte) bool {
for _, v := range ar {
if v != 0 {
return false
}
}
return true
}
// ClearBytes securely erases a byte slice by overwriting all bytes with zeros.
// This is a convenience wrapper around Clear for byte slices.
//
// This is especially important for slices because executing `mem.Clear` on
// a slice, it will only zero out the slice header structure itself, NOT the
// underlying array data that the slice points to.
//
// When we pass a byte slice s to the function Clear[T any](s *T),
// we are passing a pointer to the slice header, not a pointer to the
// underlying array. The slice header contains three fields:
// - A pointer to the underlying array
// - The length of the slice
// - The capacity of the slice
//
// mem.Clear(s) will zero out this slice header structure, but not the
// actual array data the slice points to
//
// Parameters:
// - b: A byte slice that should be securely erased
//
// Usage:
//
// key := []byte{...} // Sensitive cryptographic key
// defer mem.ClearBytes(key)
// // Use key...
func ClearBytes(b []byte) {
if len(b) == 0 {
return
}
for i := range b {
b[i] = 0
}
// Make sure the data is actually wiped before gc has time to interfere
runtime.KeepAlive(b)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffe
import (
"context"
"net/http"
"os"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
"github.com/spiffe/go-spiffe/v2/workloadapi"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/config/env"
)
// EndpointSocket returns the UNIX domain socket address for the SPIFFE
// Workload API endpoint.
//
// The function first checks for the SPIFFE_ENDPOINT_SOCKET environment
// variable. If set, it returns that value. Otherwise, it returns a default
// development
//
// socket path:
//
// "unix:///tmp/spire-agent/public/api.sock"
//
// For production deployments, especially in Kubernetes environments, it's
// recommended to set SPIFFE_ENDPOINT_SOCKET to a more restricted socket path,
// such as: "unix:///run/spire/agent/sockets/spire.sock"
//
// Default socket paths by environment:
// - Development (Linux): unix:///tmp/spire-agent/public/api.sock
// - Kubernetes: unix:///run/spire/agent/sockets/spire.sock
//
// Returns:
// - string: The UNIX domain socket address for the SPIFFE Workload API
// endpoint
//
// Environment Variables:
// - SPIFFE_ENDPOINT_SOCKET: Override the default socket path
func EndpointSocket() string {
p := os.Getenv(env.SPIFFEEndpointSocket)
if p != "" {
return p
}
return "unix:///tmp/spire-agent/public/api.sock"
}
// Source creates a new SPIFFE X.509 source and returns the associated SVID ID.
// It establishes a connection to the Workload API at the specified socket path
// and retrieves the X.509 SVID for the workload.
//
// The returned X509Source should be closed when no longer needed.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
// - socketPath: The Workload API endpoint location
// (e.g., "unix:///path/to/socket")
//
// Returns:
// - *workloadapi.X509Source: An X509Source that can be used to fetch and
// monitor X.509 SVIDs
// - string: The string representation of the current SVID ID
// - *sdkErrors.SDKError: ErrSPIFFEFailedToCreateX509Source if source creation
// fails, or ErrSPIFFEUnableToFetchX509Source if initial SVID fetch fails
func Source(ctx context.Context, socketPath string) (
*workloadapi.X509Source, string, *sdkErrors.SDKError,
) {
source, err := workloadapi.NewX509Source(ctx,
workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)))
if err != nil {
return nil, "", sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err)
}
sv, err := source.GetX509SVID()
if err != nil {
return nil, "", sdkErrors.ErrSPIFFEUnableToFetchX509Source.Wrap(err)
}
return source, sv.ID.String(), nil
}
// IDFromRequest extracts the SPIFFE ID from the TLS peer certificate of
// an HTTP request. It checks if the incoming request has a valid TLS connection
// and at least one peer certificate. The first certificate in the chain is used
// to extract the SPIFFE ID.
//
// Note: This function assumes that the request is already over a secured TLS
// connection and will fail if the TLS connection state is not available or
// the peer certificates are missing.
//
// Parameters:
// - r: The HTTP request from which the SPIFFE ID is to be extracted
//
// Returns:
// - *spiffeid.ID: The SPIFFE ID extracted from the first peer certificate,
// or nil if extraction fails
// - *sdkErrors.SDKError: ErrSPIFFENoPeerCertificates if peer certificates are
// absent, or ErrSPIFFEFailedToExtractX509SVID if extraction fails
func IDFromRequest(r *http.Request) (*spiffeid.ID, *sdkErrors.SDKError) {
tlsConnectionState := r.TLS
if len(tlsConnectionState.PeerCertificates) == 0 {
return nil, sdkErrors.ErrSPIFFENoPeerCertificates
}
id, err := x509svid.IDFromCert(tlsConnectionState.PeerCertificates[0])
if err != nil {
return nil, sdkErrors.ErrSPIFFEFailedToExtractX509SVID.Wrap(err)
}
return &id, nil
}
// CloseSource safely closes an X509Source.
//
// This function should be called when the X509Source is no longer needed,
// typically during application shutdown or cleanup. It handles nil sources
// gracefully.
//
// Parameters:
// - source: The X509Source to close, may be nil
//
// Returns:
// - *sdkErrors.SDKError: nil if successful or source is nil,
// ErrSPIFFEFailedToCloseX509Source if closure fails
func CloseSource(source *workloadapi.X509Source) *sdkErrors.SDKError {
if source == nil {
return nil
}
if err := source.Close(); err != nil {
return sdkErrors.ErrSPIFFEFailedToCloseX509Source.Wrap(err)
}
return nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffeid
import (
"strings"
"github.com/spiffe/spike-sdk-go/config/env"
)
// IsPilot checks if a given SPIFFE ID matches the SPIKE Pilot's SPIFFE ID
// pattern.
//
// This function is used for identity verification to determine if the provided
// SPIFFE ID belongs to a SPIKE pilot instance. It compares the input against
// the expected pilot SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<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:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact pilot ID
// or an extended ID with additional path segments for any of the trust
// roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/pilot"
// extendedId := "spiffe://example.org/spike/pilot/instance-0"
//
// // Both will return true
// if IsPilot(baseId) {
// // Handle pilot-specific logic
// }
//
// if IsPilot(extendedId) {
// // Also recognized as a SPIKE Pilot, with instance metadata
// }
func IsPilot(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootPilot)
for _, root := range strings.Split(trustRoots, ",") {
baseID := Pilot(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsLiteWorkload checks if a given SPIFFE ID matches the SPIKE Lite Workload's
// SPIFFE ID pattern.
//
// A SPIKE Lite workload can freely use SPIKE Nexus encryption and decryption
// RESTful APIs without needing any specific policies assigned to it. A SPIKE
// Lite workload cannot use any other SPIKE Nexus API unless a relevant policy
// is attached to it.
//
// This function is used for identity verification to determine if the provided
// SPIFFE ID belongs to a SPIKE lite workload instance. It compares the input
// against the expected lite workload SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<trustRoot>/spike/workload/role/lite"
// - Extended match with metadata:
// "spiffe://<trustRoot>/spike/workload/role/lite/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base lite workload identity.
//
// Parameters:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact lite
// workload ID or an extended ID with additional path segments for any of
// the trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/workload/role/lite"
// extendedId := "spiffe://example.org/spike/workload/role/lite/instance-0"
//
// // Both will return true
// if IsLiteWorkload(baseId) {
// // Handle lite workload-specific logic
// }
//
// if IsLiteWorkload(extendedId) {
// // Also recognized as a SPIKE Lite Workload, with instance metadata
// }
func IsLiteWorkload(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootLiteWorkload)
for _, root := range strings.Split(trustRoots, ",") {
baseID := LiteWorkload(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsPilotRecover checks if a given SPIFFE ID matches the SPIKE Pilot's
// recovery SPIFFE ID pattern.
//
// This function verifies if the provided SPIFFE ID corresponds to a SPIKE Pilot
// instance with recovery capabilities by comparing it against the expected
// recovery SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<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:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact pilot
// recovery ID or an extended ID with additional path segments for any of
// the trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/pilot/recover"
// extendedId := "spiffe://example.org/spike/pilot/recover/instance-0"
//
// // Both will return true
// if IsPilotRecover(baseId) {
// // Handle recovery-specific logic
// }
//
// if IsPilotRecover(extendedId) {
// // Also recognized as a SPIKE Pilot recovery, with instance metadata
// }
func IsPilotRecover(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootPilot)
for _, root := range strings.Split(trustRoots, ",") {
baseID := PilotRecover(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsPilotRestore checks if a given SPIFFE ID matches the SPIKE Pilot's restore
// SPIFFE ID pattern.
//
// This function verifies if the provided SPIFFE ID corresponds to a pilot
// instance with restore capabilities by comparing it against the expected
// restore SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<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:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact pilot
// restore ID or an extended ID with additional path segments for any of the
// trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/pilot/restore"
// extendedId := "spiffe://example.org/spike/pilot/restore/instance-0"
//
// // Both will return true
// if IsPilotRestore(baseId) {
// // Handle restore-specific logic
// }
//
// if IsPilotRestore(extendedId) {
// // Also recognized as a SPIKE Pilot restore, with instance metadata
// }
func IsPilotRestore(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootPilot)
for _, root := range strings.Split(trustRoots, ",") {
baseID := PilotRestore(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsBootstrap checks if a given SPIFFE ID matches the SPIKE Bootstrap's
// SPIFFE ID pattern.
//
// This function verifies if the provided SPIFFE ID corresponds to a bootstrap
// instance by comparing it against the expected bootstrap SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<trustRoot>/spike/bootstrap"
// - Extended match with metadata:
// "spiffe://<trustRoot>/spike/bootstrap/<metadata>"
//
// This allows for instance-specific identifiers while maintaining compatibility
// with the base bootstrap identity.
//
// Parameters:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact bootstrap
// ID or an extended ID with additional path segments for any of the
// trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/bootstrap"
// extendedId := "spiffe://example.org/spike/bootstrap/instance-0"
//
// // Both will return true
// if IsBootstrap(baseId) {
// // Handle bootstrap-specific logic
// }
//
// if IsBootstrap(extendedId) {
// // Also recognized as a SPIKE Bootstrap, with instance metadata
// }
func IsBootstrap(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootBootstrap)
for _, root := range strings.Split(trustRoots, ",") {
baseID := Bootstrap(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsKeeper checks if a given SPIFFE ID matches the SPIKE Keeper's SPIFFE ID.
//
// This function is used for identity verification to determine if the provided
// SPIFFE ID belongs to a SPIKE Keeper instance. It compares the input against
// the expected keeper SPIFFE ID pattern.
//
// The function supports two formats:
// - Exact match: "spiffe://<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:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the provided SPIFFE ID matches either the exact
// SPIKE Keeper's ID or an extended ID with additional path segments for any
// of the trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/keeper"
// extendedId := "spiffe://example.org/spike/keeper/instance-0"
//
// // Both will return true
// if IsKeeper(baseId) {
// // Handle keeper-specific logic
// }
//
// if IsKeeper(extendedId) {
// // Also recognized as a SPIKE Keeper, with instance metadata
// }
func IsKeeper(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootKeeper)
for _, root := range strings.Split(trustRoots, ",") {
baseID := Keeper(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// IsNexus checks if the provided SPIFFE ID matches the SPIKE Nexus SPIFFE ID.
//
// The function compares the input SPIFFE ID against the configured SPIKE Nexus
// SPIFFE ID pattern. This is typically used for validating whether a given
// identity represents the Nexus service.
//
// The function supports two formats:
// - Exact match: "spiffe://<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:
// - SPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the SPIFFE ID matches either the exact Nexus SPIFFE ID
// or an extended ID with additional path segments for any of the
// trust roots, false otherwise
//
// Example usage:
//
// baseId := "spiffe://example.org/spike/nexus"
// extendedId := "spiffe://example.org/spike/nexus/instance-0"
//
// // Both will return true
// if IsNexus(baseId) {
// // Handle Nexus-specific logic
// }
//
// if IsNexus(extendedId) {
// // Also recognized as a SPIKE Nexus, with instance metadata
// }
func IsNexus(SPIFFEID string) bool {
trustRoots := env.TrustRootFromEnv(env.TrustRootNexus)
for _, root := range strings.Split(trustRoots, ",") {
baseID := Nexus(strings.TrimSpace(root))
// Check if the ID is either exactly the base ID or starts with the base ID
// followed by "/"
if SPIFFEID == baseID || strings.HasPrefix(SPIFFEID, baseID+"/") {
return true
}
}
return false
}
// PeerCanTalkToAnyone is used for debugging purposes
func PeerCanTalkToAnyone(_, _ string) bool {
return true
}
// PeerCanTalkToKeeper checks if the provided SPIFFE ID matches the SPIKE Nexus
// SPIFFE ID.
//
// This is used as a validator in SPIKE Keeper because currently only SPIKE
// Nexus can talk to SPIKE Keeper.
//
// Parameters:
// - peerSPIFFEID: The SPIFFE ID string to check
//
// Returns:
// - bool: true if the SPIFFE ID matches SPIKE Nexus' or SPIKE Bootstrap's
// SPIFFE ID for any of the trust roots, false otherwise
func PeerCanTalkToKeeper(peerSPIFFEID string) bool {
return IsNexus(peerSPIFFEID) || IsBootstrap(peerSPIFFEID)
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package spiffeid
import (
"path"
"strings"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// Keeper constructs and returns the SPIKE Keeper's SPIFFE ID string.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/keeper"
func Keeper(trustRoot string) string {
const fName = "Keeper"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "keeper")
}
// Nexus constructs and returns the SPIFFE ID for SPIKE Nexus.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/nexus"
func Nexus(trustRoot string) string {
const fName = "Nexus"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "nexus")
}
// Pilot generates the SPIFFE ID for a SPIKE Pilot superuser role.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/pilot/role/superuser"
func Pilot(trustRoot string) string {
const fName = "Pilot"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot,
"spike", "pilot", "role", "superuser")
}
// Bootstrap generates the SPIFFE ID for a SPIKE Bootstrap role.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/bootstrap"
func Bootstrap(trustRoot string) string {
const fName = "Bootstrap"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "bootstrap")
}
// LiteWorkload generates the SPIFFE ID for a SPIKE Lite workload role.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/workload/role/lite"
func LiteWorkload(trustRoot string) string {
const fName = "LiteWorkload"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "workload", "role", "lite")
}
// PilotRecover generates the SPIFFE ID for a SPIKE Pilot recovery role.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/pilot/role/recover"
func PilotRecover(trustRoot string) string {
const fName = "PilotRecover"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "recover")
}
// PilotRestore generates the SPIFFE ID for a SPIKE Pilot restore role.
//
// Parameters:
// - trustRoot: The trust domain for the SPIFFE ID. If empty, the value is
// obtained from the environment.
//
// Returns:
// - string: The complete SPIFFE ID in the format:
// "spiffe://<trustRoot>/spike/pilot/role/restore"
func PilotRestore(trustRoot string) string {
const fName = "PilotRestore"
if trustRoot == "" {
failErr := sdkErrors.ErrSPIFFEEmptyTrustDomain
log.FatalErr(fName, *failErr)
}
if strings.Contains(trustRoot, ",") {
failErr := sdkErrors.ErrSPIFFEMultipleTrustDomains
log.FatalErr(fName, *failErr)
}
return "spiffe://" + path.Join(trustRoot, "spike", "pilot", "role", "restore")
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package strings
import (
"crypto/rand"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)
// secureRandomStringFromCharClass generates a cryptographically secure random
// string of the specified length using characters from the given character
// class.
//
// # Security: Fatal Exit on CSPRNG Failure
//
// This function uses crypto/rand.Read() as its source of randomness. If the
// cryptographic random number generator fails, this function will terminate
// the program with log.FatalErr() rather than returning an error.
//
// This design decision is intentional and critical for security:
//
// 1. CSPRNG failures indicate fundamental system compromise or misconfiguration
// 2. This function generates security-sensitive strings (passwords, tokens,
// API keys, secrets) where weak randomness would be catastrophic
// 3. Silently falling back to weaker randomness or continuing execution would
// create a false sense of security
// 4. A CSPRNG failure is an exceptional, unrecoverable system-level error
// (kernel entropy depletion, hardware failure, or system compromise)
// 5. Consistent with other security-critical operations in the SDK (Shamir
// secret sharing, SVID acquisition) that also fatal exit on failure
//
// DO NOT remove this fatal exit behavior. Allowing the function to return
// an error that could be ignored would compromise the security guarantees
// of all code using this function.
//
// Parameters:
// - charClass: character class specification supporting:
// - Predefined classes: \w (word chars), \d (digits), \x (symbols)
// - Custom ranges: "A-Z", "a-z", "0-9", or combinations like "A-Za-z0-9"
// - Individual characters: any literal characters
// - length: number of characters in the resulting string
//
// Returns:
// - string: the generated random string, empty on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrStringEmptyCharacterClass: if character class is empty
// - ErrStringInvalidRange: if character range is invalid
// - ErrStringEmptyCharacterSet: if character set is empty
//
// Note: CSPRNG failures (crypto/rand.Read) cause immediate program termination
// via log.FatalErr() for security reasons (cannot generate secure random data).
// This is intentional and critical - DO NOT remove this fatal exit behavior.
func secureRandomStringFromCharClass(
charClass string, length int,
) (string, *sdkErrors.SDKError) {
const fName = "secureRandomStringFromCharClass"
chars, err := expandCharacterClass(charClass)
if err != nil {
return "", err
}
if len(chars) == 0 {
failErr := sdkErrors.ErrStringEmptyCharacterSet
failErr.Msg = "character class resulted in empty character set"
return "", failErr
}
result := make([]byte, length)
randomBytes := make([]byte, length)
if _, randErr := rand.Read(randomBytes); randErr != nil {
failErr := sdkErrors.ErrCryptoRandomGenerationFailed.Wrap(randErr)
failErr.Msg = "cryptographic random number generator failed"
log.FatalErr(fName, *failErr)
}
for i := 0; i < length; i++ {
result[i] = chars[randomBytes[i]%byte(len(chars))]
}
return string(result), nil
}
// expandCharacterClass expands character class expressions into a string
// containing all valid characters from the class. It handles both predefined
// character classes and custom character ranges.
//
// Parameters:
// - charClass: character class expression supporting:
// - Predefined classes:
// - \w: word characters (a-z, A-Z, 0-9, and underscore)
// - \d: digits (0-9)
// - \x: symbols (printable ASCII excluding letters and digits)
// - Custom ranges:
// - Single characters: included as-is
// - Range notation: "A-Z" expands to all uppercase letters
// - Combined ranges: "A-Za-z0-9" expands to alphanumeric characters
//
// Returns:
// - string: expanded character set containing all characters from the class,
// empty on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrStringEmptyCharacterClass: if character class is empty
// - ErrStringInvalidRange: if character range is invalid (e.g., "Z-A")
// - ErrStringEmptyCharacterSet: if expansion results in empty set
func expandCharacterClass(charClass string) (string, *sdkErrors.SDKError) {
// Check for empty character class first
if len(charClass) == 0 {
failErr := sdkErrors.ErrStringEmptyCharacterClass
failErr.Msg = "character class cannot be empty"
return "", failErr
}
charSet := make(map[byte]bool) // Use map to avoid duplicates
// Handle predefined character classes
switch charClass {
case "\\w":
// Word characters: letters, digits, underscore
for c := 'a'; c <= 'z'; c++ {
charSet[byte(c)] = true
}
for c := 'A'; c <= 'Z'; c++ {
charSet[byte(c)] = true
}
for c := '0'; c <= '9'; c++ {
charSet[byte(c)] = true
}
charSet['_'] = true
case "\\d":
// Digits
for c := '0'; c <= '9'; c++ {
charSet[byte(c)] = true
}
case "\\x":
// Symbols (printable ASCII excluding letters and digits)
for c := 32; c <= 126; c++ {
ch := byte(c)
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9')) {
charSet[ch] = true
}
}
default:
// Handle character ranges and individual characters like A-Za-z0-9
i := 0
for i < len(charClass) {
if i+2 < len(charClass) && charClass[i+1] == '-' {
// Range specification
start := charClass[i]
end := charClass[i+2]
// Only allow forward ranges (`start <= end`)
if start > end {
failErr := sdkErrors.ErrStringInvalidRange
failErr.Msg = "invalid character range: start > end"
return "", failErr
}
// Add all characters in range
for c := start; c <= end; c++ {
charSet[c] = true
}
i += 3
} else {
// Single character
charSet[charClass[i]] = true
i++
}
}
}
// Convert map to slice
chars := make([]byte, 0, len(charSet))
for char := range charSet {
chars = append(chars, char)
}
// Final check for the empty result (this catches edge cases)
if len(chars) == 0 {
failErr := sdkErrors.ErrStringEmptyCharacterSet
failErr.Msg = "character class resulted in empty character set"
return "", failErr
}
return string(chars), nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package strings
import (
"regexp"
"strconv"
stdstrings "strings"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
// StringFromTemplate creates a string based on a template with embedded
// generator expressions.
//
// # Template Syntax
//
// Generator expressions follow the pattern: [character_class]{length}
// Where:
// - character_class defines which characters can be generated
// - length specifies how many characters to generate (must be a positive
// integer)
//
// # Supported Character Classes
//
// ## Predefined Classes
//
// - \w : Word characters (a-z, A-Z, 0-9, _)
// - \d : Digits (0-9)
// - \x : Symbols (printable ASCII excluding letters and digits:
// !"#$%&'()*+,-./:;<=>?@[\]^`{|}~ and space)
//
// ## Character Ranges
//
// - a-z : Lowercase letters
// - A-Z : Uppercase letters
// - 0-9 : Digits
// - a-Z : All letters (equivalent to a-zA-Z)
//
// ## Multiple Ranges and Characters
//
// You can combine multiple ranges and individual characters within a
// single class:
// - [a-zA-Z0-9] : Letters and digits
// - [A-Za-z0-6] : Letters and digits 0-6
// - [0-9a-fA-F] : Hexadecimal characters
// - [A-Ca-c1-3] : A,B,C,a,b,c,1,2,3
//
// Individual characters can be mixed with ranges:
// - [a-z_.-] : Lowercase letters plus underscore, period, and hyphen
// - [A-Z0-9!@#] : Uppercase letters, digits, and specific symbols
//
// # Template Examples
//
// StringFromTemplate("user[0-9]{4}") // "user1234"
// StringFromTemplate("pass[a-zA-Z0-9]{12}") // "passA3kL9mX2nQ8z"
// StringFromTemplate("prefix[\w]{8}suffix") // "prefixaB3_kM9Zsuffix"
// StringFromTemplate("id[0-9a-f]{8}-[0-9a-f]{4}") // "a1b2c3d4-ef56"
// StringFromTemplate("admin[a-z]{3}[A-Z]{2}[0-9]{3}") // "adminxyzAB123"
//
// # Error Conditions
//
// The function returns an error for:
// - Invalid ranges where start > end: [z-a] or [9-0]
// - Empty character classes: []
// - Invalid length specifications: non-numeric values
// - Malformed expressions: missing brackets or braces
//
// # Implementation Notes
//
// Character ranges are inclusive on both ends. When multiple ranges overlap
// (e.g., [a-zA-Z] contains both a-z and A-Z), duplicate characters are
// automatically deduplicated.
//
// Ranges must follow ASCII ordering. Cross-case ranges like [a-Z] work because
// they span the ASCII range from 'a' (97) to 'Z' (90), but this includes
// punctuation characters between uppercase and lowercase letters.
//
// # Limitations and Assumptions
//
// This implementation assumes reasonable usage patterns:
// - Character classes should be logically organized
// - Ranges should follow natural ordering (a-z, not z-a)
// - Individual characters mixed with ranges are supported but should be
// used judiciously
// - Unicode characters beyond ASCII are not explicitly supported
// - Escape sequences beyond \w, \d, \x are not supported
// - Character class negation (^) is not supported
// - POSIX character classes ([:alpha:], [:digit:]) are not supported
//
// The function prioritizes common use cases for password generation, API keys,
// tokens, and identifiers while maintaining simplicity and predictability.
//
// # Security: Fatal Exit on CSPRNG Failure
//
// This function uses crypto/rand.Read() for generating random characters. If the
// cryptographic random number generator fails, this function will terminate the
// program with log.FatalErr() rather than returning an error.
//
// This design decision is intentional and critical for security:
//
// 1. CSPRNG failures indicate fundamental system compromise or misconfiguration
// 2. This function is used for generating security-sensitive strings (passwords,
// tokens, API keys, secrets) where weak randomness would be catastrophic
// 3. Silently falling back to weaker randomness or returning an error that could
// be ignored would create a false sense of security
// 4. A CSPRNG failure is an exceptional, unrecoverable system-level error
//
// DO NOT modify this behavior to return errors for CSPRNG failures, as it would
// compromise the security guarantees of all code using this function.
//
// # Parameters
//
// template: A string containing literal text and generator expressions.
//
// Generator expressions are replaced with random characters.
//
// # Returns
//
// Returns:
// - string: The generated string with all generator expressions replaced,
// empty on error
// - *sdkErrors.SDKError: nil on success, or one of the following errors:
// - ErrStringInvalidLength: if length specification is not a valid number
// - ErrStringNegativeLength: if length is negative
// - ErrStringEmptyCharacterClass: if character class is empty
// - ErrStringInvalidRange: if character range is invalid
// - ErrStringEmptyCharacterSet: if character set is empty
//
// Note: CSPRNG failures (crypto/rand.Read) cause immediate program termination
// via log.FatalErr() for security reasons (cannot generate secure random data).
// This is intentional and critical - see "Security: Fatal Exit on CSPRNG Failure"
// section above for rationale.
func StringFromTemplate(template string) (string, *sdkErrors.SDKError) {
// Regular expression to match generator expressions like [a-z]{5} or [\w]{3}
// Modified to capture any content in braces, not just digits
// Changed + to * to allow empty character classes like []
re := regexp.MustCompile(`\[([^]]*)]\{([^}]+)}`)
result := template
// Find all matches and replace them
for {
match := re.FindStringSubmatch(result)
if match == nil {
break
}
fullMatch := match[0]
charClass := match[1]
lengthStr := match[2]
// Parse length - this will now catch non-numeric values
length, parseErr := strconv.Atoi(lengthStr)
if parseErr != nil {
failErr := sdkErrors.ErrStringInvalidLength.Wrap(parseErr)
failErr.Msg = "invalid length specification in template"
return "", failErr
}
// Validate that length is non-negative
if length < 0 {
failErr := sdkErrors.ErrStringNegativeLength
failErr.Msg = "length cannot be negative in template"
return "", failErr
}
// Generate random string based on character class
randomStr, err := secureRandomStringFromCharClass(charClass, length)
if err != nil {
return "", err
}
// Replace the first occurrence of the pattern
result = stdstrings.Replace(result, fullMatch, randomStr, 1)
}
return result, nil
}
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package system
import (
"os"
"os/signal"
"syscall"
"time"
)
// KeepAlive blocks the current goroutine until it receives either a
// SIGINT (Ctrl+C) or SIGTERM signal, enabling graceful shutdown of the
// application.
//
// The function creates a buffered channel to handle OS signals and uses
// signal.Notify to register for SIGINT and SIGTERM signals. It then blocks
// until a signal is received.
//
// An optional callback can be provided to handle the received signal. If no
// callback is provided, no action is taken when a signal is received (the
// function simply returns). This allows callers to handle logging, cleanup,
// or other actions as needed.
//
// This is typically used in the main function to prevent the program from
// exiting immediately and to ensure proper cleanup when the program is
// terminated.
//
// Parameters:
// - onSignal: Optional callback invoked when a signal is received, with the
// signal as parameter. If not provided, the function returns silently.
//
// Example usage:
//
// func main() {
// // Initialize your application
// setupApp()
//
// // Keep the application running until shutdown signal
// KeepAlive(func(sig os.Signal) {
// log.Printf("Received %v signal, shutting down gracefully...\n", sig)
// })
//
// // Perform cleanup
// cleanup()
// }
//
// Example without callback:
//
// func main() {
// setupApp()
// KeepAlive() // Simply blocks until signal, no logging
// cleanup()
// }
func KeepAlive(onSignal ...func(os.Signal)) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigChan
if len(onSignal) > 0 && onSignal[0] != nil {
onSignal[0](sig)
}
}
// WatchConfig defines the configuration for the Watch function.
type WatchConfig struct {
// WaitTimeBeforeExit specifies how long to wait after initialization
// before executing the exit action.
WaitTimeBeforeExit time.Duration
// PollInterval defines how frequently to check the initialization predicate.
PollInterval time.Duration
// InitializationPredicate is a function that returns true when the watched
// condition is met and initialization is complete.
InitializationPredicate func() bool
// ExitAction is the function to execute after the initialization predicate
// returns true and the wait time has elapsed.
ExitAction func()
// OnTick is an optional callback invoked on each polling interval.
// If nil, no action is taken on tick.
OnTick func()
// OnInitialized is an optional callback invoked when the initialization
// predicate returns true, before waiting and executing the exit action.
// If nil, no action is taken on initialization.
OnInitialized func()
}
// Watch continuously polls a condition at regular intervals and executes an
// exit action once the condition is met. It will poll using the
// InitializationPredicate function at intervals specified by PollInterval.
// When the predicate returns true, it invokes the OnInitialized callback (if
// provided), waits for WaitTimeBeforeExit duration, and then executes
// ExitAction.
//
// The OnTick callback (if provided) is invoked on each polling interval before
// checking the initialization predicate. The OnInitialized callback (if
// provided)
// is invoked when the predicate first returns true.
//
// This function runs indefinitely until the exit action is called, so it
// should typically be run in a goroutine if the exit action doesn't terminate
// the program.
//
// Example:
//
// config := WatchConfig{
// WaitTimeBeforeExit: 5 * time.Second,
// PollInterval: 1 * time.Second,
// InitializationPredicate: func() bool {
// return isServiceReady()
// },
// OnTick: func() {
// log.Println("Checking service status...")
// },
// OnInitialized: func() {
// log.Println("Service initialized successfully")
// },
// ExitAction: func() {
// log.Println("Shutting down watcher")
// os.Exit(0)
// },
// }
// go Watch(config)
func Watch(config WatchConfig) {
interval := config.PollInterval
ticker := time.NewTicker(interval)
for range ticker.C {
if config.OnTick != nil {
config.OnTick()
}
if config.InitializationPredicate() {
if config.OnInitialized != nil {
config.OnInitialized()
}
time.Sleep(config.WaitTimeBeforeExit)
config.ExitAction()
}
}
}
// \\ SPIKE: Secure your secrets with SPIFFE.
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0
package validation
import (
"regexp"
"github.com/google/uuid"
"github.com/spiffe/spike-sdk-go/api/entity/data"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
)
const validNamePattern = `^[a-zA-Z0-9-_ ]+$`
const maxNameLength = 250
const validSPIFFEIDPattern = `^\^?spiffe://[\\a-zA-Z0-9.\-*()+?\[\]]+(/[\\/a-zA-Z0-9._\-*()+?\[\]]+)*\$?$`
const validRawSPIFFEIDPattern = `^spiffe://[a-zA-Z0-9.-]+(/[a-zA-Z0-9._-]+)*$`
const maxPathPatternLength = 500
const validPathPattern = `^[a-zA-Z0-9._\-/^$()?+*|[\]{}\\]+$`
const validPath = `^[a-zA-Z0-9._\-/()?+*|[\]{}\\]+$`
// ValidateName checks if the provided name meets length and format constraints.
//
// The name must be between 1 and 250 characters and contain only alphanumeric
// characters, hyphens, underscores, and spaces.
//
// Parameters:
// - name: The name string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if name is empty, exceeds 250 characters, or
// contains invalid characters
func ValidateName(name string) *sdkErrors.SDKError {
// Validate length
if len(name) == 0 || len(name) > maxNameLength {
return sdkErrors.ErrDataInvalidInput
}
// Validate format
if match, _ := regexp.MatchString(validNamePattern, name); !match {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidateSPIFFEIDPattern validates whether the given SPIFFE ID pattern string
// conforms to the expected format.
//
// The pattern may include regex special characters for matching multiple
// SPIFFE IDs.
// It must start with "spiffe://" and follow the SPIFFE ID specification with
// optional regex metacharacters.
//
// Parameters:
// - SPIFFEIDPattern: The SPIFFE ID pattern string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if the pattern does not conform to the expected
// format
func ValidateSPIFFEIDPattern(SPIFFEIDPattern string) *sdkErrors.SDKError {
// Validate SPIFFEIDPattern
if match, _ := regexp.MatchString(
validSPIFFEIDPattern, SPIFFEIDPattern); !match {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidateSPIFFEID validates if the given SPIFFE ID matches the expected
// format.
//
// Unlike ValidateSPIFFEIDPattern, this function validates raw SPIFFE IDs
// without regex metacharacters. The ID must strictly conform to the SPIFFE
// specification:
// "spiffe://<trust-domain>/<path>".
//
// Parameters:
// - SPIFFEID: The SPIFFE ID string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if the SPIFFE ID does not conform to the expected
// format
func ValidateSPIFFEID(SPIFFEID string) *sdkErrors.SDKError {
if match, _ := regexp.MatchString(
validRawSPIFFEIDPattern, SPIFFEID); !match {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidatePathPattern validates the given path pattern string for correctness.
//
// This function is used for validating path patterns that may contain regex
// metacharacters for matching multiple paths. The path pattern must be between
// 1 and 500 characters and may include regex anchors (^, $) and other regex
// special characters (?, +, *, |, [], {}, \, etc.) along with alphanumeric
// characters, underscores, hyphens, forward slashes, and periods.
//
// Use ValidatePath instead if you need to validate literal paths without
// regex anchors.
//
// Parameters:
// - pathPattern: The path pattern string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if the pattern is empty, exceeds 500 characters, or
// contains invalid characters
func ValidatePathPattern(pathPattern string) *sdkErrors.SDKError {
// Validate length
if len(pathPattern) == 0 || len(pathPattern) > maxPathPatternLength {
return sdkErrors.ErrDataInvalidInput
}
// Validate format
// Allow regex special characters along with alphanumeric and basic symbols
if match, _ := regexp.MatchString(validPathPattern, pathPattern); !match {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidatePath checks if the given path is valid based on predefined rules.
//
// This function validates paths that should not contain regex anchor
// metacharacters (^ or $). Unlike ValidatePathPattern, this function is for
// validating literal paths. The path must be between 1 and 500 characters.
//
// Note: While this function excludes regex anchors (^, $), it still allows
// other special characters that may appear in actual paths such as ?, +, *,
// |, [], {}, \, /, etc. Use ValidatePathPattern if you need to validate
// patterns that include regex anchors.
//
// Parameters:
// - path: The path string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if the path is empty, exceeds 500 characters, or
// contains invalid characters
func ValidatePath(path string) *sdkErrors.SDKError {
if len(path) == 0 || len(path) > maxPathPatternLength {
return sdkErrors.ErrDataInvalidInput
}
if match, _ := regexp.MatchString(validPath, path); !match {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidatePolicyID verifies if the given policy ID is a valid UUID format.
//
// The policy ID must conform to the UUID specification (RFC 4122).
// This function uses the google/uuid package for validation.
//
// Parameters:
// - policyID: The policy ID string to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if valid, or one of the following errors:
// - ErrDataInvalidInput: if the policy ID is not a valid UUID
func ValidatePolicyID(policyID string) *sdkErrors.SDKError {
err := uuid.Validate(policyID)
if err != nil {
return sdkErrors.ErrDataInvalidInput
}
return nil
}
// ValidatePermissions checks if all provided permissions are valid.
//
// Permissions are compared against a predefined list of allowed permissions:
// - PermissionList: list secrets
// - PermissionRead: read secret values
// - PermissionWrite: create/update secrets
// - PermissionSuper: administrative access
//
// Parameters:
// - permissions: Slice of policy permissions to validate
//
// Returns:
// - *sdkErrors.SDKError: nil if all permissions are valid, or one of the
// following errors:
// - ErrDataInvalidInput: if any permission is not in the allowed list
func ValidatePermissions(
permissions []data.PolicyPermission,
) *sdkErrors.SDKError {
allowedPermissions := []data.PolicyPermission{
data.PermissionList,
data.PermissionRead,
data.PermissionWrite,
data.PermissionSuper,
}
for _, permission := range permissions {
isAllowed := false
for _, allowedPermission := range allowedPermissions {
if permission == allowedPermission {
isAllowed = true
break
}
}
if !isAllowed {
return sdkErrors.ErrDataInvalidInput
}
}
return nil
}