keysource.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package nodetoken
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "runtime"
  9. "strings"
  10. )
  11. // KeySource loads a startup keyring from a protected file or environment.
  12. // Keys are never accepted on the command line.
  13. type KeySource interface {
  14. Load() (*Keyring, error)
  15. }
  16. // keyFile identifies the active key and all base64-encoded rotation keys.
  17. type keyFile struct {
  18. Active string `json:"active"`
  19. Keys map[string]string `json:"keys"`
  20. }
  21. func parseKeyring(active string, b64keys map[string]string) (*Keyring, error) {
  22. if err := validateKeyID(active); err != nil {
  23. return nil, fmt.Errorf("nodetoken: active key id: %w", err)
  24. }
  25. if active == "" {
  26. return nil, errors.New("nodetoken: key source has no active key id")
  27. }
  28. if len(b64keys) == 0 {
  29. return nil, errors.New("nodetoken: key source has no keys")
  30. }
  31. kr := &Keyring{ActiveID: active, Keys: make(map[string][keyLen]byte, len(b64keys))}
  32. for id, b64 := range b64keys {
  33. if err := validateKeyID(id); err != nil {
  34. return nil, fmt.Errorf("nodetoken: key id %q: %w", id, err)
  35. }
  36. raw, err := decodeKey(b64)
  37. if err != nil {
  38. return nil, fmt.Errorf("nodetoken: key %q: %w", id, err)
  39. }
  40. kr.Keys[id] = raw
  41. }
  42. if _, ok := kr.Keys[active]; !ok {
  43. return nil, fmt.Errorf("nodetoken: active key %q absent from keys", active)
  44. }
  45. return kr, nil
  46. }
  47. func validateKeyID(id string) error {
  48. if id == "" {
  49. return errors.New("must not be empty")
  50. }
  51. if strings.Contains(id, ":") {
  52. return errors.New("must not contain ':'")
  53. }
  54. return nil
  55. }
  56. func decodeKey(b64 string) ([keyLen]byte, error) {
  57. var out [keyLen]byte
  58. raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64))
  59. if err != nil {
  60. // tolerate url-safe / unpadded encodings too
  61. if raw2, err2 := base64.RawStdEncoding.DecodeString(strings.TrimSpace(b64)); err2 == nil {
  62. raw = raw2
  63. } else {
  64. return out, fmt.Errorf("base64 decode: %w", err)
  65. }
  66. }
  67. if len(raw) != keyLen {
  68. return out, fmt.Errorf("key must be %d bytes, got %d", keyLen, len(raw))
  69. }
  70. copy(out[:], raw)
  71. return out, nil
  72. }
  73. // FileKeySource accepts only key files that are mode 0600 or stricter. Windows has
  74. // no such bits (Stat reports 0666), so there the file's NTFS ACL is what guards it.
  75. type FileKeySource struct {
  76. Path string
  77. }
  78. func (f FileKeySource) Load() (*Keyring, error) {
  79. info, err := os.Stat(f.Path)
  80. if err != nil {
  81. return nil, fmt.Errorf("nodetoken: stat key file %s: %w", f.Path, err)
  82. }
  83. if perm := info.Mode().Perm(); runtime.GOOS != "windows" && perm&0o077 != 0 {
  84. return nil, fmt.Errorf("nodetoken: key file %s has insecure mode %#o (want 0600)", f.Path, perm)
  85. }
  86. data, err := os.ReadFile(f.Path)
  87. if err != nil {
  88. return nil, fmt.Errorf("nodetoken: read key file %s: %w", f.Path, err)
  89. }
  90. var kf keyFile
  91. if err := json.Unmarshal(data, &kf); err != nil {
  92. return nil, fmt.Errorf("nodetoken: parse key file %s: %w", f.Path, err)
  93. }
  94. return parseKeyring(kf.Active, kf.Keys)
  95. }
  96. // EnvKeySource reads a single base64 32-byte key from an environment variable.
  97. // The key id is fixed ("env"); for multi-key rotation prefer a key file.
  98. type EnvKeySource struct {
  99. Var string
  100. }
  101. func (e EnvKeySource) Load() (*Keyring, error) {
  102. v := strings.TrimSpace(os.Getenv(e.Var))
  103. if v == "" {
  104. return nil, fmt.Errorf("nodetoken: env %s is empty", e.Var)
  105. }
  106. return parseKeyring("env", map[string]string{"env": v})
  107. }