사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
package credentialstore
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// ErrEnvelopeKeyUnavailable is returned when the key registry cannot be reached or is unconfigured.
|
|
ErrEnvelopeKeyUnavailable = errors.New("credentialstore: envelope key unavailable")
|
|
|
|
// ErrUnknownEnvelopeKey is returned when the referenced key ID or version is not registered.
|
|
ErrUnknownEnvelopeKey = errors.New("credentialstore: unknown envelope key")
|
|
|
|
// ErrInvalidSecretEnvelope is returned when envelope fields fail validation.
|
|
ErrInvalidSecretEnvelope = errors.New("credentialstore: invalid secret envelope")
|
|
)
|
|
|
|
// EnvelopeKeyRegistry checks whether an envelope encryption key is registered.
|
|
type EnvelopeKeyRegistry interface {
|
|
HasEnvelopeKey(ctx context.Context, keyID string, keyVersion uint64) (bool, error)
|
|
}
|
|
|
|
const (
|
|
// algorithmAES256GCM is the only authenticated-encryption algorithm the
|
|
// store accepts. It is declared locally so credentialstore never depends on
|
|
// the higher-layer credentialseal package.
|
|
algorithmAES256GCM = "AES-256-GCM"
|
|
|
|
// minNonceLength is the minimum AES-GCM nonce length (the 96-bit standard).
|
|
// Shorter nonces weaken GCM and are rejected at the persistence boundary.
|
|
minNonceLength = 12
|
|
)
|
|
|
|
type unavailableKeyRegistry struct{}
|
|
|
|
func (unavailableKeyRegistry) HasEnvelopeKey(ctx context.Context, keyID string, keyVersion uint64) (bool, error) {
|
|
return false, ErrEnvelopeKeyUnavailable
|
|
}
|
|
|
|
// SecretEnvelope holds opaque encrypted secret payload and key metadata.
|
|
type SecretEnvelope struct {
|
|
Algorithm string `json:"algorithm"`
|
|
KeyID string `json:"key_id"`
|
|
KeyVersion uint64 `json:"key_version"`
|
|
Nonce []byte `json:"nonce"`
|
|
Ciphertext []byte `json:"ciphertext"`
|
|
AAD []byte `json:"aad,omitempty"`
|
|
}
|
|
|
|
// Validate checks that the envelope carries a supported authenticated-encryption
|
|
// algorithm, a registered-shaped key revision, a full-length nonce, and
|
|
// ciphertext. It stays secret-blind: it inspects only opaque metadata and never
|
|
// the plaintext. AAD is optional metadata and is not required to be present.
|
|
func (e SecretEnvelope) Validate() error {
|
|
algorithm := strings.TrimSpace(e.Algorithm)
|
|
if algorithm == "" {
|
|
return fmt.Errorf("%w: algorithm is required", ErrInvalidSecretEnvelope)
|
|
}
|
|
if algorithm != algorithmAES256GCM {
|
|
return fmt.Errorf("%w: unsupported algorithm", ErrInvalidSecretEnvelope)
|
|
}
|
|
if strings.TrimSpace(e.KeyID) == "" {
|
|
return fmt.Errorf("%w: key_id is required", ErrInvalidSecretEnvelope)
|
|
}
|
|
if e.KeyVersion == 0 {
|
|
return fmt.Errorf("%w: key_version must be greater than 0", ErrInvalidSecretEnvelope)
|
|
}
|
|
if len(e.Nonce) < minNonceLength {
|
|
return fmt.Errorf("%w: nonce is too short", ErrInvalidSecretEnvelope)
|
|
}
|
|
if len(e.Ciphertext) == 0 {
|
|
return fmt.Errorf("%w: ciphertext is required", ErrInvalidSecretEnvelope)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateEnvelopeKey checks that the envelope is valid and its key ID and version are registered.
|
|
func (s *Store) validateEnvelopeKey(ctx context.Context, env SecretEnvelope) error {
|
|
if err := env.Validate(); err != nil {
|
|
return err
|
|
}
|
|
registry := s.KeyRegistry()
|
|
ok, err := registry.HasEnvelopeKey(ctx, env.KeyID, env.KeyVersion)
|
|
if err != nil {
|
|
if errors.Is(err, ErrEnvelopeKeyUnavailable) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%w: %v", ErrEnvelopeKeyUnavailable, err)
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("%w: key_id=%s version=%d", ErrUnknownEnvelopeKey, env.KeyID, env.KeyVersion)
|
|
}
|
|
return nil
|
|
}
|