push.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. /*
  2. * Copyright (c) 2026, 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 push implements server entry push payloads, which support pushing
  20. // server entries to clients through external distribution channels. Push
  21. // payloads use the compact packed CBOR server entry representation.
  22. //
  23. // Each server entry has an optional prioritize dial flag which is equivalent
  24. // to dsl.VersionedServerEntryTag.PrioritizedDial.
  25. //
  26. // Payloads include an expiry date to ensure freshness and mitigate replay
  27. // attacks. The entire payload is digitally signed, and an obfuscation layer
  28. // is added on top.
  29. package push
  30. import (
  31. "bytes"
  32. "crypto/aes"
  33. "crypto/cipher"
  34. "crypto/ed25519"
  35. "crypto/rand"
  36. "crypto/sha256"
  37. "encoding/base64"
  38. "sort"
  39. "time"
  40. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  41. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
  42. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  43. "github.com/fxamacker/cbor/v2"
  44. )
  45. const (
  46. obfuscationKeySize = 32
  47. signaturePublicKeyDigestSize = 8
  48. maxPaddingLimit = 65535
  49. signatureSize = signaturePublicKeyDigestSize + ed25519.SignatureSize
  50. )
  51. // Payload is a push payload, consisting of a list of server entries. To
  52. // ensure stale server entries and stale dial prioritizations are not
  53. // imported, the list has an expiry timestamp.
  54. type Payload struct {
  55. Expires time.Time `cbor:"1,keyasint,omitempty"`
  56. PrioritizedServerEntries []*PrioritizedServerEntry `cbor:"2,keyasint,omitempty"`
  57. }
  58. // SignedPayload is Payload with a digital signature.
  59. type SignedPayload struct {
  60. Signature []byte `cbor:"1,keyasint,omitempty"`
  61. Payload []byte `cbor:"2,keyasint,omitempty"`
  62. Padding []byte `cbor:"3,keyasint,omitempty"`
  63. }
  64. // PrioritizedServerEntry is a server entry paired with a server entry source
  65. // description and a dial prioritization indication. PrioritizeDial is
  66. // equivalent to DSL prioritized dials.
  67. type PrioritizedServerEntry struct {
  68. ServerEntryFields protocol.PackedServerEntryFields `cbor:"1,keyasint,omitempty"`
  69. Source string `cbor:"2,keyasint,omitempty"`
  70. PrioritizeDial bool `cbor:"3,keyasint,omitempty"`
  71. }
  72. // ServerEntryImporter is a callback that is invoked for each server entry in
  73. // an imported push payload.
  74. type ServerEntryImporter func(
  75. packedServerEntryFields protocol.PackedServerEntryFields,
  76. source string,
  77. prioritizeDial bool) error
  78. // GenerateKeys generates a new obfuscation key and signature key pair for
  79. // push payloads.
  80. func GenerateKeys() (
  81. payloadObfuscationKey string,
  82. payloadSignaturePublicKey string,
  83. payloadSignaturePrivateKey string,
  84. err error) {
  85. obfuscationKey := make([]byte, obfuscationKeySize)
  86. _, err = rand.Read(obfuscationKey)
  87. if err != nil {
  88. return "", "", "", errors.Trace(err)
  89. }
  90. publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
  91. if err != nil {
  92. return "", "", "", errors.Trace(err)
  93. }
  94. return base64.StdEncoding.EncodeToString(obfuscationKey),
  95. base64.StdEncoding.EncodeToString(publicKey),
  96. base64.StdEncoding.EncodeToString(privateKey),
  97. nil
  98. }
  99. // ImportPushPayload imports the input push payload. The ServerEntryImporter
  100. // callback is invoked for each imported server entry and its associated
  101. // source and prioritizeDial data.
  102. func ImportPushPayload(
  103. payloadObfuscationKey string,
  104. payloadSignaturePublicKey string,
  105. obfuscatedPayload []byte,
  106. serverEntryImporter ServerEntryImporter) (int, error) {
  107. obfuscationKey, err := base64.StdEncoding.DecodeString(
  108. payloadObfuscationKey)
  109. if err == nil && len(obfuscationKey) != obfuscationKeySize {
  110. err = errors.TraceNew("unexpected obfuscation key size")
  111. }
  112. if err != nil {
  113. return 0, errors.Trace(err)
  114. }
  115. publicKey, err := base64.StdEncoding.DecodeString(
  116. payloadSignaturePublicKey)
  117. if err == nil && len(publicKey) != ed25519.PublicKeySize {
  118. err = errors.TraceNew("unexpected signature public key size")
  119. }
  120. if err != nil {
  121. return 0, errors.Trace(err)
  122. }
  123. blockCipher, err := aes.NewCipher(obfuscationKey)
  124. if err != nil {
  125. return 0, errors.Trace(err)
  126. }
  127. aead, err := cipher.NewGCM(blockCipher)
  128. if err != nil {
  129. return 0, errors.Trace(err)
  130. }
  131. if len(obfuscatedPayload) < aead.NonceSize() {
  132. return 0, errors.TraceNew("missing nonce")
  133. }
  134. cborSignedPayload, err := aead.Open(
  135. nil,
  136. obfuscatedPayload[:aead.NonceSize()],
  137. obfuscatedPayload[aead.NonceSize():],
  138. nil)
  139. if err != nil {
  140. return 0, errors.Trace(err)
  141. }
  142. var signedPayload SignedPayload
  143. err = cbor.Unmarshal(cborSignedPayload, &signedPayload)
  144. if err != nil {
  145. return 0, errors.Trace(err)
  146. }
  147. if len(signedPayload.Signature) !=
  148. signaturePublicKeyDigestSize+ed25519.SignatureSize {
  149. return 0, errors.TraceNew("invalid signature size")
  150. }
  151. publicKeyDigest := sha256.Sum256(publicKey)
  152. expectedPublicKeyID := publicKeyDigest[:signaturePublicKeyDigestSize]
  153. if !bytes.Equal(
  154. expectedPublicKeyID,
  155. signedPayload.Signature[:signaturePublicKeyDigestSize]) {
  156. return 0, errors.TraceNew("unexpected signature public key ID")
  157. }
  158. if !ed25519.Verify(
  159. publicKey,
  160. signedPayload.Payload,
  161. signedPayload.Signature[signaturePublicKeyDigestSize:]) {
  162. return 0, errors.TraceNew("invalid signature")
  163. }
  164. var payload Payload
  165. err = cbor.Unmarshal(signedPayload.Payload, &payload)
  166. if err != nil {
  167. return 0, errors.Trace(err)
  168. }
  169. if payload.Expires.Before(time.Now().UTC()) {
  170. return 0, errors.TraceNew("payload expired")
  171. }
  172. imported := 0
  173. for _, prioritizedServerEntry := range payload.PrioritizedServerEntries {
  174. err := serverEntryImporter(
  175. prioritizedServerEntry.ServerEntryFields,
  176. prioritizedServerEntry.Source,
  177. prioritizedServerEntry.PrioritizeDial)
  178. if err != nil {
  179. return imported, errors.Trace(err)
  180. }
  181. imported += 1
  182. }
  183. return imported, nil
  184. }
  185. // MakePushPayloadsResult is the output from MakePushPayloads.
  186. type MakePushPayloadsResult struct {
  187. // Payloads contains generated obfuscated push payloads.
  188. Payloads [][]byte
  189. // PayloadEntryCounts contains the number of entries in each payload, aligned
  190. // by index with Payloads.
  191. PayloadEntryCounts []int
  192. // SkippedIndexes contains original input indexes for entries that could not
  193. // fit into a payload when max payload size is enforced.
  194. SkippedIndexes []int
  195. }
  196. type payloadBuffers struct {
  197. nonce []byte
  198. signature []byte
  199. obfuscation []byte
  200. padding []byte
  201. }
  202. type sortablePrioritizedServerEntry struct {
  203. entry *PrioritizedServerEntry
  204. originalIndex int
  205. encodedSize int
  206. }
  207. // PushPayloadMaker caches expensive initialization (base64 decoding, AES-GCM
  208. // cipher creation, SHA256 hashing) so that multiple MakePayloads calls can
  209. // reuse the same state.
  210. //
  211. // PushPayloadMaker is safe for concurrent use. Each MakePayloads call
  212. // allocates its own mutable buffers via a fresh payloadBuffers.
  213. type PushPayloadMaker struct {
  214. aead cipher.AEAD
  215. privateKey ed25519.PrivateKey
  216. publicKeyID []byte
  217. }
  218. // NewPushPayloadMaker creates a PushPayloadMaker by performing the expensive
  219. // one-time initialization: base64 decoding all keys, validating sizes, and
  220. // creating the AES-GCM cipher.
  221. func NewPushPayloadMaker(
  222. payloadObfuscationKey string,
  223. payloadSignaturePublicKey string,
  224. payloadSignaturePrivateKey string,
  225. ) (*PushPayloadMaker, error) {
  226. obfuscationKey, err := base64.StdEncoding.DecodeString(
  227. payloadObfuscationKey)
  228. if err == nil && len(obfuscationKey) != obfuscationKeySize {
  229. err = errors.TraceNew("unexpected obfuscation key size")
  230. }
  231. if err != nil {
  232. return nil, errors.Trace(err)
  233. }
  234. publicKey, err := base64.StdEncoding.DecodeString(
  235. payloadSignaturePublicKey)
  236. if err == nil && len(publicKey) != ed25519.PublicKeySize {
  237. err = errors.TraceNew("unexpected signature public key size")
  238. }
  239. if err != nil {
  240. return nil, errors.Trace(err)
  241. }
  242. privateKey, err := base64.StdEncoding.DecodeString(
  243. payloadSignaturePrivateKey)
  244. if err == nil && len(privateKey) != ed25519.PrivateKeySize {
  245. err = errors.TraceNew("unexpected signature private key size")
  246. }
  247. if err != nil {
  248. return nil, errors.Trace(err)
  249. }
  250. blockCipher, err := aes.NewCipher(obfuscationKey)
  251. if err != nil {
  252. return nil, errors.Trace(err)
  253. }
  254. aead, err := cipher.NewGCM(blockCipher)
  255. if err != nil {
  256. return nil, errors.Trace(err)
  257. }
  258. publicKeyDigest := sha256.Sum256(publicKey)
  259. return &PushPayloadMaker{
  260. aead: aead,
  261. privateKey: privateKey,
  262. publicKeyID: publicKeyDigest[:signaturePublicKeyDigestSize],
  263. }, nil
  264. }
  265. // MakePushPayloads generates obfuscated push payloads from prioritized server
  266. // entries, reusing the cached key material and cipher from the maker.
  267. //
  268. // When maxPayloadSizeBytes <= 0, all entries are encoded into a single payload.
  269. //
  270. // When maxPayloadSizeBytes > 0, entries are packed into multiple payloads using
  271. // an RF(2) (random-fit with 2 candidates) strategy. Entries that cannot
  272. // fit by themselves under maxPayloadSizeBytes are skipped and reported in the
  273. // returned result metadata.
  274. func (m *PushPayloadMaker) MakePushPayloads(
  275. minPadding int,
  276. maxPadding int,
  277. TTL time.Duration,
  278. prioritizedServerEntries []*PrioritizedServerEntry,
  279. maxPayloadSizeBytes int) (MakePushPayloadsResult, error) {
  280. result := MakePushPayloadsResult{}
  281. if len(prioritizedServerEntries) == 0 {
  282. return result, nil
  283. }
  284. if minPadding > maxPadding || maxPadding > maxPaddingLimit {
  285. return result, errors.TraceNew("invalid min/max padding")
  286. }
  287. bufs := &payloadBuffers{
  288. nonce: make([]byte, m.aead.NonceSize()),
  289. }
  290. expires := time.Now().Add(TTL).UTC()
  291. // maxPayloadSizeBytes <= 0 means no payload size cap is enforced.
  292. if maxPayloadSizeBytes <= 0 {
  293. paddingSize := prng.Range(minPadding, maxPadding)
  294. payload, err := m.buildObfuscatedPayload(
  295. bufs, prioritizedServerEntries, expires, paddingSize)
  296. if err != nil {
  297. return result, errors.Trace(err)
  298. }
  299. result.Payloads = append(result.Payloads, payload)
  300. result.PayloadEntryCounts = append(
  301. result.PayloadEntryCounts, len(prioritizedServerEntries))
  302. return result, nil
  303. }
  304. // Pre-compute the CBOR-encoded size of the expires timestamp.
  305. expiresEncoded, err := protocol.CBOREncoding.Marshal(expires)
  306. if err != nil {
  307. return result, errors.Trace(err)
  308. }
  309. expiresEncodedSize := len(expiresEncoded)
  310. // Compute encoded sizes for each PrioritizedServerEntry.
  311. serverEntries := make(
  312. []sortablePrioritizedServerEntry, 0, len(prioritizedServerEntries))
  313. for i, entry := range prioritizedServerEntries {
  314. encodedEntry, err := protocol.CBOREncoding.Marshal(entry)
  315. if err != nil {
  316. return result, errors.Trace(err)
  317. }
  318. serverEntries = append(serverEntries, sortablePrioritizedServerEntry{
  319. entry: entry,
  320. originalIndex: i,
  321. encodedSize: len(encodedEntry),
  322. })
  323. }
  324. // Sort server entries by decreasing size, this significantly
  325. // increases packing quality but doesn't bias the bins themselves.
  326. sort.Slice(serverEntries, func(i, j int) bool {
  327. if serverEntries[i].encodedSize == serverEntries[j].encodedSize {
  328. return serverEntries[i].originalIndex < serverEntries[j].originalIndex
  329. }
  330. return serverEntries[i].encodedSize > serverEntries[j].encodedSize
  331. })
  332. // Worst-case each PrioritizedServerEntry gets its own bin.
  333. type payloadBin struct {
  334. serverEntries []*PrioritizedServerEntry
  335. paddingSize int
  336. // sumServerEntrySize is the total encoded size of all server
  337. // entries in this bin, used to compute the obfuscated payload size.
  338. sumServerEntrySize int
  339. }
  340. bins := make([]payloadBin, 0, len(serverEntries))
  341. binOrder := make([]int, 0, len(serverEntries))
  342. type candidate struct {
  343. binIndex int
  344. size int
  345. }
  346. for _, sortedServerEntry := range serverEntries {
  347. // RF(2): randomly sample bins, collect the first 2 that fit,
  348. // and pick the tightest (least remaining space).
  349. // Grow and reset binOrder to [0..len(bins)).
  350. binOrder = binOrder[:0]
  351. for i := range bins {
  352. binOrder = append(binOrder, i)
  353. }
  354. prng.Shuffle(len(binOrder), func(i, j int) {
  355. binOrder[i], binOrder[j] = binOrder[j], binOrder[i]
  356. })
  357. var candidates [2]candidate
  358. numCandidates := 0
  359. for _, bi := range binOrder {
  360. if numCandidates >= 2 {
  361. break
  362. }
  363. // Arithmetically compute the size of the obfuscated payload size
  364. // without the expensive marshalling and encryption.
  365. size := m.computeObfuscatedPayloadSize(
  366. expiresEncodedSize,
  367. len(bins[bi].serverEntries)+1,
  368. bins[bi].sumServerEntrySize+sortedServerEntry.encodedSize,
  369. bins[bi].paddingSize)
  370. if size <= maxPayloadSizeBytes {
  371. candidates[numCandidates] = candidate{
  372. binIndex: bi,
  373. size: size,
  374. }
  375. numCandidates++
  376. }
  377. }
  378. if numCandidates > 0 {
  379. // Pick tightest fit (highest size).
  380. best := 0
  381. if numCandidates == 2 &&
  382. candidates[1].size > candidates[0].size {
  383. best = 1
  384. }
  385. bi := candidates[best].binIndex
  386. bins[bi].serverEntries = append(bins[bi].serverEntries, sortedServerEntry.entry)
  387. bins[bi].sumServerEntrySize += sortedServerEntry.encodedSize
  388. continue
  389. }
  390. // Server entry did not fit into existing bins,
  391. // create a new bin with minPadding. Random padding is
  392. // applied after packing to avoid wasting bin capacity.
  393. paddingSize := minPadding
  394. size := m.computeObfuscatedPayloadSize(
  395. expiresEncodedSize, 1, sortedServerEntry.encodedSize, paddingSize)
  396. if size > maxPayloadSizeBytes {
  397. result.SkippedIndexes = append(
  398. result.SkippedIndexes, sortedServerEntry.originalIndex)
  399. continue
  400. }
  401. bins = append(bins, payloadBin{
  402. serverEntries: []*PrioritizedServerEntry{sortedServerEntry.entry},
  403. paddingSize: paddingSize,
  404. sumServerEntrySize: sortedServerEntry.encodedSize,
  405. })
  406. }
  407. // Apply random padding to each bin, respecting maxPayloadSizeBytes.
  408. noPadding := minPadding == 0 && maxPadding == 0
  409. if !noPadding {
  410. for i := range bins {
  411. randomPadding := prng.Range(minPadding, maxPadding)
  412. if randomPadding <= bins[i].paddingSize {
  413. continue
  414. }
  415. size := m.computeObfuscatedPayloadSize(
  416. expiresEncodedSize, len(bins[i].serverEntries), bins[i].sumServerEntrySize, randomPadding)
  417. if size <= maxPayloadSizeBytes {
  418. bins[i].paddingSize = randomPadding
  419. } else {
  420. // Reduce padding to fit within maxPayloadSizeBytes.
  421. excess := size - maxPayloadSizeBytes
  422. reduced := randomPadding - excess
  423. if reduced > bins[i].paddingSize {
  424. bins[i].paddingSize = reduced
  425. }
  426. }
  427. }
  428. }
  429. result.Payloads = make([][]byte, 0, len(bins))
  430. result.PayloadEntryCounts = make([]int, 0, len(bins))
  431. for _, bin := range bins {
  432. payload, err := m.buildObfuscatedPayload(
  433. bufs, bin.serverEntries, expires, bin.paddingSize)
  434. if err != nil {
  435. return result, errors.Trace(err)
  436. }
  437. // Apply a hard correctness check.
  438. if len(payload) > maxPayloadSizeBytes {
  439. return result, errors.TraceNew(
  440. "internal error: payload size exceeds max")
  441. }
  442. result.Payloads = append(result.Payloads, payload)
  443. result.PayloadEntryCounts = append(
  444. result.PayloadEntryCounts, len(bin.serverEntries))
  445. }
  446. return result, nil
  447. }
  448. func (m *PushPayloadMaker) buildObfuscatedPayload(
  449. bufs *payloadBuffers,
  450. prioritizedServerEntries []*PrioritizedServerEntry,
  451. expires time.Time,
  452. paddingSize int) ([]byte, error) {
  453. obfuscatedPayload, err := m.makeObfuscatedPayload(
  454. bufs, prioritizedServerEntries, expires, paddingSize)
  455. if err != nil {
  456. return nil, errors.Trace(err)
  457. }
  458. return append([]byte(nil), obfuscatedPayload...), nil
  459. }
  460. // cborHeaderSize returns the size of a CBOR definite-length header for the
  461. // given count or length value.
  462. func cborHeaderSize(n int) int {
  463. switch {
  464. case n <= 23:
  465. return 1
  466. case n <= 255:
  467. return 2
  468. case n <= 65535:
  469. return 3
  470. default:
  471. return 5
  472. }
  473. }
  474. // computeObfuscatedPayloadSize computes the exact obfuscated payload size
  475. // arithmetically from pre-computed component sizes, avoiding CBOR marshaling.
  476. //
  477. // The obfuscated payload structure is:
  478. //
  479. // nonce || AES-GCM(CBOR(SignedPayload{ Signature, CBOR(Payload), Padding })) || tag
  480. func (m *PushPayloadMaker) computeObfuscatedPayloadSize(
  481. expiresEncodedSize int,
  482. numEntries int,
  483. entrySizeSum int,
  484. paddingSize int) int {
  485. // Payload = map { 1: expires, 2: array(entries) }
  486. // With omitempty, the entries field is omitted when numEntries == 0.
  487. payloadFields := 1 // Expires
  488. payloadBody := 1 + expiresEncodedSize
  489. if numEntries > 0 {
  490. payloadFields++
  491. payloadBody += 1 + cborHeaderSize(numEntries) + entrySizeSum
  492. }
  493. payloadSize := cborHeaderSize(payloadFields) + payloadBody
  494. // SignedPayload = map { 1: bstr(signature), 2: bstr(payload), [3: bstr(padding)] }
  495. sigLen := signatureSize
  496. spFields := 2
  497. spBody := 1 + cborHeaderSize(sigLen) + sigLen +
  498. 1 + cborHeaderSize(payloadSize) + payloadSize
  499. if paddingSize > 0 {
  500. spFields++
  501. spBody += 1 + cborHeaderSize(paddingSize) + paddingSize
  502. }
  503. signedPayloadSize := cborHeaderSize(spFields) + spBody
  504. return m.aead.NonceSize() + signedPayloadSize + m.aead.Overhead()
  505. }
  506. func (m *PushPayloadMaker) makeObfuscatedPayload(
  507. bufs *payloadBuffers,
  508. prioritizedServerEntries []*PrioritizedServerEntry,
  509. expires time.Time,
  510. paddingSize int) ([]byte, error) {
  511. payload := Payload{
  512. Expires: expires,
  513. PrioritizedServerEntries: prioritizedServerEntries,
  514. }
  515. cborPayload, err := protocol.CBOREncoding.Marshal(&payload)
  516. if err != nil {
  517. return nil, errors.Trace(err)
  518. }
  519. signature := ed25519.Sign(m.privateKey, cborPayload)
  520. bufs.signature = bufs.signature[:0]
  521. bufs.signature = append(bufs.signature, m.publicKeyID...)
  522. bufs.signature = append(bufs.signature, signature...)
  523. signedPayload := SignedPayload{
  524. Signature: bufs.signature,
  525. Payload: cborPayload,
  526. }
  527. if paddingSize < 0 || paddingSize > maxPaddingLimit {
  528. return nil, errors.TraceNew("invalid padding size")
  529. }
  530. if paddingSize > 0 {
  531. if bufs.padding == nil {
  532. bufs.padding = make([]byte, maxPaddingLimit)
  533. }
  534. signedPayload.Padding = bufs.padding[:paddingSize]
  535. }
  536. cborSignedPayload, err := protocol.CBOREncoding.Marshal(&signedPayload)
  537. if err != nil {
  538. return nil, errors.Trace(err)
  539. }
  540. // The faster common/prng is appropriate for obfuscation.
  541. prng.Read(bufs.nonce[:])
  542. bufs.obfuscation = bufs.obfuscation[:0]
  543. bufs.obfuscation = append(bufs.obfuscation, bufs.nonce...)
  544. bufs.obfuscation = m.aead.Seal(
  545. bufs.obfuscation,
  546. bufs.nonce[:],
  547. cborSignedPayload,
  548. nil)
  549. return bufs.obfuscation, nil
  550. }