verae-mcp-timestamping-serv.../pkg/digest/digest.go
George Lambert e4432fc555 feat: module scaffold and digest normalize/guard
pkg/digest (not internal/) so Task 2 pkg/tsnats can import helpers.
2026-09-29 11:43:22 -04:00

72 lines
1.5 KiB
Go

package digest
import (
"fmt"
"regexp"
"sort"
"strings"
)
// ForbiddenKeys are argument names rejected by digests-only tools.
var ForbiddenKeys = []string{
"content",
"bytes",
"file",
"data",
"path",
"body",
"payload",
"blob",
"archive",
}
var (
hex64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
forbiddenLower = func() map[string]struct{} {
m := make(map[string]struct{}, len(ForbiddenKeys))
for _, k := range ForbiddenKeys {
m[strings.ToLower(k)] = struct{}{}
}
return m
}()
)
// NormalizeSHA256 strips an optional "sha256:" prefix, lowercases, and
// requires exactly 64 hexadecimal characters.
func NormalizeSHA256(s string) (string, error) {
raw := strings.TrimSpace(s)
if strings.HasPrefix(strings.ToLower(raw), "sha256:") {
raw = strings.TrimSpace(raw[len("sha256:"):])
}
raw = strings.ToLower(raw)
if !hex64.MatchString(raw) {
return "", fmt.Errorf("sha256 must be 64 hexadecimal characters")
}
return raw, nil
}
// AssertDigestOnly rejects maps that include any ForbiddenKeys (case-insensitive).
func AssertDigestOnly(args map[string]any) error {
if args == nil {
return nil
}
var bad []string
seen := make(map[string]struct{})
for k := range args {
lk := strings.ToLower(k)
if _, ok := forbiddenLower[lk]; ok {
if _, dup := seen[lk]; !dup {
seen[lk] = struct{}{}
bad = append(bad, k)
}
}
}
if len(bad) == 0 {
return nil
}
sort.Strings(bad)
return fmt.Errorf(
"client refuses to send content/bytes to stamp/verify/hash_report; got: %s",
strings.Join(bad, ", "),
)
}