chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package deviceauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const deviceSecretReferencePrefix = "device-secret://"
|
||||
|
||||
var deviceSecretNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
|
||||
|
||||
// DirectorySecretResolver resolves a per-device credential from a controlled
|
||||
// secret mount. It is suitable for Docker/Kubernetes Secrets and cloud KMS CSI
|
||||
// drivers: the database stores only device-secret://<name>, never secret bytes.
|
||||
// Files are read for every verification so an atomic secret-mount rotation is
|
||||
// observed without restarting the API process.
|
||||
type DirectorySecretResolver struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewDirectorySecretResolver(root string) (*DirectorySecretResolver, error) {
|
||||
if strings.TrimSpace(root) == "" || !filepath.IsAbs(root) {
|
||||
return nil, errors.New("device secret directory must be an absolute path")
|
||||
}
|
||||
absolute, err := filepath.Abs(filepath.Clean(root))
|
||||
if err != nil {
|
||||
return nil, errors.New("resolve device secret directory")
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(absolute)
|
||||
if err != nil {
|
||||
return nil, errors.New("resolve device secret directory")
|
||||
}
|
||||
info, err := os.Stat(resolved)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil, errors.New("device secret directory is not readable")
|
||||
}
|
||||
return &DirectorySecretResolver{root: resolved}, nil
|
||||
}
|
||||
|
||||
func (r *DirectorySecretResolver) Resolve(ctx context.Context, reference string) ([]byte, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(reference, deviceSecretReferencePrefix) {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
name := strings.TrimPrefix(reference, deviceSecretReferencePrefix)
|
||||
if !deviceSecretNamePattern.MatchString(name) || name == "." || name == ".." {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
candidate := filepath.Join(r.root, name)
|
||||
resolved, err := filepath.EvalSymlinks(candidate)
|
||||
if err != nil || !pathWithin(r.root, resolved) {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
file, err := os.Open(resolved)
|
||||
if err != nil {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
defer file.Close()
|
||||
secret, err := io.ReadAll(io.LimitReader(file, 4097))
|
||||
if err != nil || len(secret) > 4096 {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
secret = bytes.TrimRight(secret, "\r\n")
|
||||
if len(secret) < 32 {
|
||||
return nil, ErrCredentialInvalid
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte(nil), secret...), nil
|
||||
}
|
||||
|
||||
func pathWithin(root, candidate string) bool {
|
||||
relative, err := filepath.Rel(root, candidate)
|
||||
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)
|
||||
}
|
||||
Reference in New Issue
Block a user