disco.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package key
  4. import (
  5. "bytes"
  6. "crypto/subtle"
  7. "fmt"
  8. "go4.org/mem"
  9. "golang.org/x/crypto/curve25519"
  10. "golang.org/x/crypto/nacl/box"
  11. "tailscale.com/types/structs"
  12. )
  13. const (
  14. // discoPublicHexPrefix is the prefix used to identify a
  15. // hex-encoded disco public key.
  16. //
  17. // This prefix is used in the control protocol, so cannot be
  18. // changed.
  19. discoPublicHexPrefix = "discokey:"
  20. // DiscoPublicRawLen is the length in bytes of a DiscoPublic, when
  21. // serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
  22. DiscoPublicRawLen = 32
  23. )
  24. // DiscoPrivate is a disco key, used for peer-to-peer path discovery.
  25. type DiscoPrivate struct {
  26. _ structs.Incomparable // because == isn't constant-time
  27. k [32]byte
  28. }
  29. // NewDisco creates and returns a new disco private key.
  30. func NewDisco() DiscoPrivate {
  31. var ret DiscoPrivate
  32. rand(ret.k[:])
  33. // Key used for nacl seal/open, so needs to be clamped.
  34. clamp25519Private(ret.k[:])
  35. return ret
  36. }
  37. // IsZero reports whether k is the zero value.
  38. func (k DiscoPrivate) IsZero() bool {
  39. return k.Equal(DiscoPrivate{})
  40. }
  41. // Equal reports whether k and other are the same key.
  42. func (k DiscoPrivate) Equal(other DiscoPrivate) bool {
  43. return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
  44. }
  45. // Public returns the DiscoPublic for k.
  46. // Panics if DiscoPrivate is zero.
  47. func (k DiscoPrivate) Public() DiscoPublic {
  48. if k.IsZero() {
  49. panic("can't take the public key of a zero DiscoPrivate")
  50. }
  51. var ret DiscoPublic
  52. curve25519.ScalarBaseMult(&ret.k, &k.k)
  53. return ret
  54. }
  55. // Shared returns the DiscoShared for communication between k and p.
  56. func (k DiscoPrivate) Shared(p DiscoPublic) DiscoShared {
  57. if k.IsZero() || p.IsZero() {
  58. panic("can't compute shared secret with zero keys")
  59. }
  60. var ret DiscoShared
  61. box.Precompute(&ret.k, &p.k, &k.k)
  62. return ret
  63. }
  64. // SortedPairOfDiscoPublic is a lexicographically sorted container of two
  65. // [DiscoPublic] keys.
  66. type SortedPairOfDiscoPublic struct {
  67. k [2]DiscoPublic
  68. }
  69. // Get returns the underlying keys.
  70. func (s SortedPairOfDiscoPublic) Get() [2]DiscoPublic {
  71. return s.k
  72. }
  73. // NewSortedPairOfDiscoPublic returns a SortedPairOfDiscoPublic from a and b.
  74. func NewSortedPairOfDiscoPublic(a, b DiscoPublic) SortedPairOfDiscoPublic {
  75. s := SortedPairOfDiscoPublic{}
  76. if a.Compare(b) < 0 {
  77. s.k[0] = a
  78. s.k[1] = b
  79. } else {
  80. s.k[0] = b
  81. s.k[1] = a
  82. }
  83. return s
  84. }
  85. func (s SortedPairOfDiscoPublic) String() string {
  86. return fmt.Sprintf("%s <=> %s", s.k[0].ShortString(), s.k[1].ShortString())
  87. }
  88. // Equal returns true if s and b are equal, otherwise it returns false.
  89. func (s SortedPairOfDiscoPublic) Equal(b SortedPairOfDiscoPublic) bool {
  90. for i := range s.k {
  91. if s.k[i].Compare(b.k[i]) != 0 {
  92. return false
  93. }
  94. }
  95. return true
  96. }
  97. // DiscoPublic is the public portion of a DiscoPrivate.
  98. type DiscoPublic struct {
  99. k [32]byte
  100. }
  101. // DiscoPublicFromRaw32 parses a 32-byte raw value as a DiscoPublic.
  102. //
  103. // This should be used only when deserializing a DiscoPublic from a
  104. // binary protocol.
  105. func DiscoPublicFromRaw32(raw mem.RO) DiscoPublic {
  106. if raw.Len() != 32 {
  107. panic("input has wrong size")
  108. }
  109. var ret DiscoPublic
  110. raw.Copy(ret.k[:])
  111. return ret
  112. }
  113. // IsZero reports whether k is the zero value.
  114. func (k DiscoPublic) IsZero() bool {
  115. return k == DiscoPublic{}
  116. }
  117. // Raw32 returns k encoded as 32 raw bytes.
  118. //
  119. // Deprecated: only needed for a temporary compat shim in tailcfg, do
  120. // not add more uses.
  121. func (k DiscoPublic) Raw32() [32]byte {
  122. return k.k
  123. }
  124. // ShortString returns the Tailscale conventional debug representation
  125. // of a disco key.
  126. func (k DiscoPublic) ShortString() string {
  127. if k.IsZero() {
  128. return ""
  129. }
  130. return fmt.Sprintf("d:%x", k.k[:8])
  131. }
  132. // AppendTo appends k, serialized as a 32-byte binary value, to
  133. // buf. Returns the new slice.
  134. func (k DiscoPublic) AppendTo(buf []byte) []byte {
  135. return append(buf, k.k[:]...)
  136. }
  137. // String returns the output of MarshalText as a string.
  138. func (k DiscoPublic) String() string {
  139. bs, err := k.MarshalText()
  140. if err != nil {
  141. panic(err)
  142. }
  143. return string(bs)
  144. }
  145. // Compare returns an integer comparing DiscoPublic k and l lexicographically.
  146. // The result will be 0 if k == other, -1 if k < other, and +1 if k > other.
  147. // This is useful for situations requiring only one node in a pair to perform
  148. // some operation, e.g. probing UDP path lifetime.
  149. func (k DiscoPublic) Compare(other DiscoPublic) int {
  150. return bytes.Compare(k.k[:], other.k[:])
  151. }
  152. // AppendText implements encoding.TextAppender.
  153. func (k DiscoPublic) AppendText(b []byte) ([]byte, error) {
  154. return appendHexKey(b, discoPublicHexPrefix, k.k[:]), nil
  155. }
  156. // MarshalText implements encoding.TextMarshaler.
  157. func (k DiscoPublic) MarshalText() ([]byte, error) {
  158. return k.AppendText(nil)
  159. }
  160. // MarshalText implements encoding.TextUnmarshaler.
  161. func (k *DiscoPublic) UnmarshalText(b []byte) error {
  162. return parseHex(k.k[:], mem.B(b), mem.S(discoPublicHexPrefix))
  163. }
  164. type DiscoShared struct {
  165. _ structs.Incomparable // because == isn't constant-time
  166. k [32]byte
  167. }
  168. // Equal reports whether k and other are the same key.
  169. func (k DiscoShared) Equal(other DiscoShared) bool {
  170. return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
  171. }
  172. func (k DiscoShared) IsZero() bool {
  173. return k.Equal(DiscoShared{})
  174. }
  175. // Seal wraps cleartext into a NaCl box (see
  176. // golang.org/x/crypto/nacl), using k as the shared secret and a
  177. // random nonce.
  178. func (k DiscoShared) Seal(cleartext []byte) (ciphertext []byte) {
  179. if k.IsZero() {
  180. panic("can't seal with zero key")
  181. }
  182. var nonce [24]byte
  183. rand(nonce[:])
  184. return box.SealAfterPrecomputation(nonce[:], cleartext, &nonce, &k.k)
  185. }
  186. // Open opens the NaCl box ciphertext, which must be a value created
  187. // by Seal, and returns the inner cleartext if ciphertext is a valid
  188. // box using shared secret k.
  189. func (k DiscoShared) Open(ciphertext []byte) (cleartext []byte, ok bool) {
  190. if k.IsZero() {
  191. panic("can't open with zero key")
  192. }
  193. if len(ciphertext) < 24 {
  194. return nil, false
  195. }
  196. nonce := (*[24]byte)(ciphertext)
  197. return box.OpenAfterPrecomputation(nil, ciphertext[24:], nonce, &k.k)
  198. }