2025-05-22 12:14:16 -07:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package key
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/subtle"
|
|
|
|
|
"encoding/hex"
|
2025-06-09 15:16:37 -04:00
|
|
|
|
"encoding/json"
|
2025-05-22 12:14:16 -07:00
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"go4.org/mem"
|
|
|
|
|
"tailscale.com/types/structs"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var ErrInvalidMeshKey = errors.New("invalid mesh key")
|
|
|
|
|
|
|
|
|
|
// DERPMesh is a mesh key, used for inter-DERP-node communication and for
|
|
|
|
|
// privileged DERP clients.
|
|
|
|
|
type DERPMesh struct {
|
|
|
|
|
_ structs.Incomparable // == isn't constant-time
|
|
|
|
|
k [32]byte // 64-digit hexadecimal numbers fit in 32 bytes
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 15:16:37 -04:00
|
|
|
|
func (k DERPMesh) MarshalJSON() ([]byte, error) {
|
|
|
|
|
return json.Marshal(k.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (k *DERPMesh) UnmarshalJSON(data []byte) error {
|
|
|
|
|
var s string
|
|
|
|
|
json.Unmarshal(data, &s)
|
|
|
|
|
|
|
|
|
|
if len(s) != 64 {
|
|
|
|
|
return fmt.Errorf("%w: JSON Unmarshal failure, mesh key must be a 64-digit hexadecimal number", ErrInvalidMeshKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decoded, err := hex.DecodeString(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%w: JSON Unmarshal failure, %v", ErrInvalidMeshKey, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(decoded) != 32 {
|
|
|
|
|
return fmt.Errorf("%w: JSON Unmarshal failure, mesh key must be 32 bytes", ErrInvalidMeshKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
copy(k.k[:], decoded)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-22 12:14:16 -07:00
|
|
|
|
// DERPMeshFromRaw32 parses a 32-byte raw value as a DERP mesh key.
|
2025-06-09 15:16:37 -04:00
|
|
|
|
func DERPMeshFromRaw32(raw mem.RO) (DERPMesh, error) {
|
2025-05-22 12:14:16 -07:00
|
|
|
|
if raw.Len() != 32 {
|
2025-06-09 15:16:37 -04:00
|
|
|
|
return DERPMesh{}, fmt.Errorf("%w: must be 32 bytes", ErrInvalidMeshKey)
|
2025-05-22 12:14:16 -07:00
|
|
|
|
}
|
|
|
|
|
var ret DERPMesh
|
|
|
|
|
raw.Copy(ret.k[:])
|
2025-06-09 15:16:37 -04:00
|
|
|
|
return ret, nil
|
2025-05-22 12:14:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseDERPMesh parses a DERP mesh key from a string.
|
|
|
|
|
// This function trims whitespace around the string.
|
|
|
|
|
// If the key is not a 64-digit hexadecimal number, ErrInvalidMeshKey is returned.
|
|
|
|
|
func ParseDERPMesh(key string) (DERPMesh, error) {
|
|
|
|
|
key = strings.TrimSpace(key)
|
|
|
|
|
if len(key) != 64 {
|
|
|
|
|
return DERPMesh{}, fmt.Errorf("%w: must be 64-digit hexadecimal number", ErrInvalidMeshKey)
|
|
|
|
|
}
|
|
|
|
|
decoded, err := hex.DecodeString(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return DERPMesh{}, fmt.Errorf("%w: %v", ErrInvalidMeshKey, err)
|
|
|
|
|
}
|
2025-06-09 15:16:37 -04:00
|
|
|
|
return DERPMeshFromRaw32(mem.B(decoded))
|
2025-05-22 12:14:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsZero reports whether k is the zero value.
|
|
|
|
|
func (k DERPMesh) IsZero() bool {
|
|
|
|
|
return k.Equal(DERPMesh{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Equal reports whether k and other are the same key.
|
|
|
|
|
func (k DERPMesh) Equal(other DERPMesh) bool {
|
|
|
|
|
// Compare mesh keys in constant time to prevent timing attacks.
|
|
|
|
|
// Since mesh keys are a fixed length, we don’t need to be concerned
|
|
|
|
|
// about timing attacks on client mesh keys that are the wrong length.
|
|
|
|
|
// See https://github.com/tailscale/corp/issues/28720
|
|
|
|
|
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String returns k as a hex-encoded 64-digit number.
|
|
|
|
|
func (k DERPMesh) String() string {
|
|
|
|
|
return hex.EncodeToString(k.k[:])
|
|
|
|
|
}
|