obfuscation.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /*
  2. * Copyright (c) 2023, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package inproxy
  20. import (
  21. "crypto/aes"
  22. "crypto/cipher"
  23. "crypto/rand"
  24. "crypto/sha256"
  25. "encoding/base64"
  26. "encoding/binary"
  27. "io"
  28. "sync"
  29. "time"
  30. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  31. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
  32. "github.com/bits-and-blooms/bloom/v3"
  33. "golang.org/x/crypto/hkdf"
  34. )
  35. const (
  36. obfuscationSessionPacketNonceSize = 12
  37. obfuscationAntiReplayTimePeriod = 10 * time.Minute
  38. obfuscationAntiReplayHistorySize = 10_000_000
  39. )
  40. // ObfuscationSecret is shared, semisecret value used in obfuscation layers.
  41. type ObfuscationSecret [32]byte
  42. // ObfuscationSecretFromString returns an ObfuscationSecret given its string encoding.
  43. func ObfuscationSecretFromString(s string) (ObfuscationSecret, error) {
  44. var secret ObfuscationSecret
  45. return secret, errors.Trace(fromBase64String(s, secret[:]))
  46. }
  47. // String emits ObfuscationSecrets as base64.
  48. func (secret ObfuscationSecret) String() string {
  49. return base64.RawStdEncoding.EncodeToString([]byte(secret[:]))
  50. }
  51. // GenerateRootObfuscationSecret creates a new ObfuscationSecret using
  52. // crypto/rand.
  53. func GenerateRootObfuscationSecret() (ObfuscationSecret, error) {
  54. var secret ObfuscationSecret
  55. _, err := rand.Read(secret[:])
  56. if err != nil {
  57. return secret, errors.Trace(err)
  58. }
  59. return secret, nil
  60. }
  61. // antiReplayTimeFactorPeriodSeconds is variable, to enable overriding the value in
  62. // tests. This value should not be overridden outside of test
  63. // cases.
  64. var antiReplayTimeFactorPeriodSeconds = int64(
  65. obfuscationAntiReplayTimePeriod / time.Second)
  66. // deriveObfuscationSecret derives an obfuscation secret from the root secret,
  67. // and context.
  68. func deriveObfuscationSecret(
  69. rootObfuscationSecret ObfuscationSecret,
  70. context string) (ObfuscationSecret, error) {
  71. var key ObfuscationSecret
  72. _, err := io.ReadFull(
  73. hkdf.New(sha256.New, rootObfuscationSecret[:], nil, []byte(context)), key[:])
  74. if err != nil {
  75. return key, errors.Trace(err)
  76. }
  77. return key, nil
  78. }
  79. // deriveSessionPacketObfuscationSecret derives a common session obfuscation
  80. // secret for either end of a session. Set isInitiator to true for packets
  81. // sent or received by the initator; and false for packets sent or received
  82. // by a responder. Set isObfuscating to true for sent packets, and false for
  83. // received packets.
  84. func deriveSessionPacketObfuscationSecret(
  85. rootObfuscationSecret ObfuscationSecret,
  86. isInitiator bool,
  87. isObfuscating bool) (ObfuscationSecret, error) {
  88. // Upstream is packets from the initiator to the responder; or,
  89. // (isInitiator && isObfuscating) || (!isInitiator && !isObfuscating)
  90. isUpstream := (isInitiator == isObfuscating)
  91. // Derive distinct keys for each flow direction, to ensure that the two
  92. // flows can't simply be xor'd.
  93. context := "in-proxy-session-packet-intiator-to-responder"
  94. if !isUpstream {
  95. context = "in-proxy-session-packet-responder-to-initiator"
  96. }
  97. key, err := deriveObfuscationSecret(rootObfuscationSecret, context)
  98. if err != nil {
  99. return ObfuscationSecret{}, errors.Trace(err)
  100. }
  101. return key, nil
  102. }
  103. // deriveSessionPacketObfuscationSecrets derives both send and receive
  104. // obfuscation secrets.
  105. func deriveSessionPacketObfuscationSecrets(
  106. rootObfuscationSecret ObfuscationSecret,
  107. isInitiator bool) (ObfuscationSecret, ObfuscationSecret, error) {
  108. send, err := deriveSessionPacketObfuscationSecret(
  109. rootObfuscationSecret, isInitiator, true)
  110. if err != nil {
  111. return ObfuscationSecret{}, ObfuscationSecret{}, errors.Trace(err)
  112. }
  113. receive, err := deriveSessionPacketObfuscationSecret(
  114. rootObfuscationSecret, isInitiator, false)
  115. if err != nil {
  116. return ObfuscationSecret{}, ObfuscationSecret{}, errors.Trace(err)
  117. }
  118. return send, receive, nil
  119. }
  120. // obfuscateSessionPacket wraps a session packet with an obfuscation layer
  121. // which provides:
  122. //
  123. // - indistiguishability from fully random
  124. // - random padding
  125. // - anti-replay
  126. //
  127. // The full-random and padding properties make obfuscated packets appropriate
  128. // to embed in otherwise plaintext transports, such as HTTP, without being
  129. // trivially fingerprintable.
  130. //
  131. // While Noise protocol sessions messages have nonces and associated
  132. // anti-replay for nonces, this measure doen't cover the session handshake,
  133. // so an independent anti-replay mechanism is implemented here.
  134. func obfuscateSessionPacket(
  135. obfuscationSecret ObfuscationSecret,
  136. isInitiator bool,
  137. packet []byte,
  138. paddingMin int,
  139. paddingMax int) ([]byte, error) {
  140. obfuscatedPacket := make([]byte, obfuscationSessionPacketNonceSize)
  141. _, err := prng.Read(obfuscatedPacket[:])
  142. if err != nil {
  143. return nil, errors.Trace(err)
  144. }
  145. // Initiators add a timestamp within the obfuscated packet. The responder
  146. // uses this value to discard potentially replayed packets which are
  147. // outside the time range of the reponder's anti-replay history.
  148. // TODO: add a consistent (per-session), random offset to timestamps for
  149. // privacy?
  150. var timestampedPacket []byte
  151. if isInitiator {
  152. timestampedPacket = binary.AppendVarint(nil, time.Now().Unix())
  153. }
  154. paddingSize := prng.Range(paddingMin, paddingMax)
  155. paddedPacket := binary.AppendUvarint(timestampedPacket, uint64(paddingSize))
  156. paddedPacket = append(paddedPacket, make([]byte, paddingSize)...)
  157. paddedPacket = append(paddedPacket, packet...)
  158. block, err := aes.NewCipher(obfuscationSecret[:])
  159. if err != nil {
  160. return nil, errors.Trace(err)
  161. }
  162. aesgcm, err := cipher.NewGCM(block)
  163. if err != nil {
  164. return nil, errors.Trace(err)
  165. }
  166. obfuscatedPacket = aesgcm.Seal(
  167. obfuscatedPacket,
  168. obfuscatedPacket[:obfuscationSessionPacketNonceSize],
  169. paddedPacket,
  170. nil)
  171. return obfuscatedPacket, nil
  172. }
  173. // deobfuscateSessionPacket deobfuscates a session packet obfuscated with
  174. // obfuscateSessionPacket and the same deobfuscateSessionPacket.
  175. //
  176. // Responders must supply an obfuscationReplayHistory, which checks for
  177. // replayed session packets (within the time factor). Responders should drop
  178. // into anti-probing response behavior when deobfuscateSessionPacket returns
  179. // an error: the obfuscated packet may have been created by a prober without
  180. // the correct secret; or replayed by a prober.
  181. func deobfuscateSessionPacket(
  182. obfuscationSecret ObfuscationSecret,
  183. isInitiator bool,
  184. replayHistory *obfuscationReplayHistory,
  185. obfuscatedPacket []byte) ([]byte, error) {
  186. // A responder must provide a relay history, or it's misconfigured.
  187. if isInitiator == (replayHistory != nil) {
  188. return nil, errors.TraceNew("unexpected replay history")
  189. }
  190. // imitateDeobfuscateSessionPacketDuration is called in early failure
  191. // cases to imitate the elapsed time of lookups and cryptographic
  192. // operations that would otherwise be skipped. This is intended to
  193. // mitigate timing attacks by probers.
  194. //
  195. // Limitation: this doesn't result in a constant time.
  196. if len(obfuscatedPacket) < obfuscationSessionPacketNonceSize {
  197. imitateDeobfuscateSessionPacketDuration(replayHistory)
  198. return nil, errors.TraceNew("invalid nonce")
  199. }
  200. nonce := obfuscatedPacket[:obfuscationSessionPacketNonceSize]
  201. if replayHistory != nil && replayHistory.Lookup(nonce) {
  202. imitateDeobfuscateSessionPacketDuration(nil)
  203. return nil, errors.TraceNew("replayed nonce")
  204. }
  205. // As an AEAD, AES-GCM authenticates that the sender used the expected
  206. // key, and so has the root obfuscation secret.
  207. block, err := aes.NewCipher(obfuscationSecret[:])
  208. if err != nil {
  209. return nil, errors.Trace(err)
  210. }
  211. aesgcm, err := cipher.NewGCM(block)
  212. if err != nil {
  213. return nil, errors.Trace(err)
  214. }
  215. plaintext, err := aesgcm.Open(
  216. nil,
  217. nonce,
  218. obfuscatedPacket[obfuscationSessionPacketNonceSize:],
  219. nil)
  220. if err != nil {
  221. return nil, errors.Trace(err)
  222. }
  223. n := 0
  224. offset := 0
  225. timestamp := int64(0)
  226. if replayHistory != nil {
  227. timestamp, n = binary.Varint(plaintext[offset:])
  228. if timestamp == 0 && n <= 0 {
  229. return nil, errors.TraceNew("invalid timestamp")
  230. }
  231. offset += n
  232. }
  233. paddingSize, n := binary.Uvarint(plaintext[offset:])
  234. if n < 1 {
  235. return nil, errors.TraceNew("invalid padding size")
  236. }
  237. offset += n
  238. if len(plaintext[offset:]) < int(paddingSize) {
  239. return nil, errors.TraceNew("invalid padding")
  240. }
  241. offset += int(paddingSize)
  242. if replayHistory != nil {
  243. // Accept the initiator's timestamp only if it's within +/-
  244. // antiReplayTimeFactorPeriodSeconds/2 of the responder's clock. This
  245. // step discards packets that are outside the range of the replay history.
  246. now := time.Now().Unix()
  247. if timestamp+antiReplayTimeFactorPeriodSeconds/2 < now {
  248. return nil, errors.TraceNew("timestamp behind")
  249. }
  250. if timestamp-antiReplayTimeFactorPeriodSeconds/2 > now {
  251. return nil, errors.TraceNew("timestamp ahead")
  252. }
  253. // Now that it's validated, add this packet to the replay history. The
  254. // nonce is expected to be unique, so it's used as the history key.
  255. replayHistory.Insert(nonce)
  256. }
  257. return plaintext[offset:], nil
  258. }
  259. func imitateDeobfuscateSessionPacketDuration(replayHistory *obfuscationReplayHistory) {
  260. // Limitations: only one block is decrypted; crypto/aes or
  261. // crypto/cipher.GCM may not be constant time, depending on hardware
  262. // support; at best, this all-zeros invocation will make it as far as
  263. // GCM.Open, and not check padding.
  264. const (
  265. blockSize = 16
  266. tagSize = 16
  267. )
  268. var secret ObfuscationSecret
  269. var packet [obfuscationSessionPacketNonceSize + blockSize + tagSize]byte
  270. if replayHistory != nil {
  271. _ = replayHistory.Lookup(packet[:obfuscationSessionPacketNonceSize])
  272. }
  273. _, _ = deobfuscateSessionPacket(secret, true, nil, packet[:])
  274. }
  275. // obfuscationReplayHistory provides a lookup for recently observed obfuscated
  276. // session packet nonces. History is maintained for
  277. // 2*antiReplayTimeFactorPeriodSeconds; it's assumed that older packets, if
  278. // replayed, will fail to deobfuscate due to using an expired timestamp.
  279. type obfuscationReplayHistory struct {
  280. mutex sync.Mutex
  281. filters [2]*bloom.BloomFilter
  282. currentFilter int
  283. switchTime time.Time
  284. }
  285. func newObfuscationReplayHistory() *obfuscationReplayHistory {
  286. // Replay history is implemented using bloom filters, which use fixed
  287. // space overhead, and less space overhead than storing nonces explictly
  288. // under anticipated loads. With bloom filters, false positive lookups
  289. // are possible, but false negative lookups are not. So there's a small
  290. // chance that a non-replayed nonce will be flagged as in the history,
  291. // but no chance that a replayed nonce will pass as not in the history.
  292. //
  293. // With obfuscationAntiReplayHistorySize set to 10M and a false positive
  294. // rate of 0.001, the session_test test case with 10k clients making 100
  295. // requests each all within one time period consistently produces no
  296. // false positives.
  297. //
  298. // Memory overhead is approximately 18MB per bloom filter, so 18MB x 2.
  299. // From:
  300. //
  301. // m, _ := bloom.EstimateParameters(10_000_000, 0.001) --> 143775876
  302. // bitset.New(143775876).BinaryStorageSize() --> approx. 18MB in terms of
  303. // underlying bits-and-blooms/bitset.BitSet
  304. //
  305. // To accomodate the rolling time factor window, there are two rotating
  306. // bloom filters.
  307. return &obfuscationReplayHistory{
  308. filters: [2]*bloom.BloomFilter{
  309. bloom.NewWithEstimates(obfuscationAntiReplayHistorySize, 0.001),
  310. bloom.NewWithEstimates(obfuscationAntiReplayHistorySize, 0.001),
  311. },
  312. currentFilter: 0,
  313. switchTime: time.Now(),
  314. }
  315. }
  316. func (h *obfuscationReplayHistory) Insert(value []byte) {
  317. h.mutex.Lock()
  318. defer h.mutex.Unlock()
  319. h.switchFilters()
  320. h.filters[h.currentFilter].Add(value)
  321. }
  322. func (h *obfuscationReplayHistory) Lookup(value []byte) bool {
  323. h.mutex.Lock()
  324. defer h.mutex.Unlock()
  325. h.switchFilters()
  326. return h.filters[0].Test(value) ||
  327. h.filters[1].Test(value)
  328. }
  329. func (h *obfuscationReplayHistory) switchFilters() {
  330. // Assumes caller holds h.mutex lock.
  331. now := time.Now()
  332. if h.switchTime.Before(now.Add(-time.Duration(antiReplayTimeFactorPeriodSeconds) * time.Second)) {
  333. h.currentFilter = (h.currentFilter + 1) % 2
  334. h.filters[h.currentFilter].ClearAll()
  335. h.switchTime = now
  336. }
  337. }