keysource.go 3.2 KB

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