Signal Protocol Adalah Lapisan Kriptografi, Bukan Protokol Transport

Service Go dapat mengirim pesan melalui WebSocket, HTTP, WebRTC, MQTT, atau antrean store-and-forward tanpa satu pun transport tersebut menyediakan end-to-end encryption. Enkripsi transport seperti TLS melindungi koneksi antara peer jaringan. Signal Protocol menangani batas yang berbeda: isi pesan dienkripsi pada satu endpoint dan tetap berupa ciphertext sampai endpoint penerima memproses sesi kriptografi yang sesuai.

Perbedaan itu menentukan posisi Signal Protocol dalam aplikasi. Relay dapat mengautentikasi akun, merutekan envelope, menyimpan ciphertext yang belum terkirim, dan menerapkan kuota, tetapi relay tidak perlu memiliki conversation key yang dibutuhkan untuk mendapatkan kembali plaintext pesan.

State sesi berada di atas koneksi jaringan

Koneksi WebSocket memiliki lifecycle: connect, bertukar frame, disconnect, lalu reconnect. Sesi Signal memiliki lifecycle kriptografi yang harus bertahan melewati event jaringan tersebut.

Menganggap keduanya sebagai state yang sama menghasilkan desain rapuh. Reconnect socket tidak boleh diam-diam mereset cryptographic ratchet, dan perpindahan dari Wi-Fi ke transport seluler tidak boleh mengharuskan identity key baru.

Batas yang berguna adalah:

application message
       |
       v
cryptographic session
       |
       | encrypted envelope
       v
transport adapter
       |
       v
relay / message queue
       |
       v
transport adapter
       |
       | encrypted envelope
       v
cryptographic session
       |
       v
application message

Transport menangani delivery. Session layer menangani key, message counter, ratchet state, dan authenticated encryption.

Untuk codebase Go, pemisahan ini lebih penting daripada pemilihan package networking tertentu. Interface transport dapat memindahkan opaque bytes tanpa mengetahui bagaimana bytes tersebut dihasilkan.

type Transport interface {
    Send(ctx context.Context, recipient string, envelope []byte) error
}

type SessionStore interface {
    Load(ctx context.Context, peer string) ([]byte, error)
    Save(ctx context.Context, peer string, state []byte) error
}

Interface tersebut sengaja tidak mengasumsikan implementasi Signal tertentu. Cryptographic state harus dikelola oleh implementasi protokol, bukan direkonstruksi dari field aplikasi.

Messaging asinkron memerlukan key material yang dipublikasikan

Pesan terenkripsi pertama menghadirkan masalah khusus: penerima mungkin sedang offline.

X3DH dirancang untuk kondisi asinkron ini. Penerima memublikasikan identity key, signed prekey, signature-nya, dan persediaan one-time prekey ke server. Pengirim dapat mengambil prekey bundle dan membentuk shared secret tanpa mengharuskan kedua perangkat online pada saat yang sama.

Konstruksi X3DH yang lebih lama tetap berguna untuk memahami arsitektur ini:

perangkat Bob
   |
   | identity public key
   | signed prekey
   | one-time prekeys
   v
prekey service
   ^
   | ambil bundle
   |
perangkat Alice
   |
   | bentuk shared secret
   v
initial encrypted message

Spesifikasi Signal saat ini juga mendefinisikan PQXDH, yang menambahkan komponen post-quantum pada asynchronous key agreement. Karena itu, aplikasi sebaiknya tidak menyamakan seluruh Signal Protocol dengan satu handshake historis. Session establishment dan fase ratcheting merupakan mekanisme terpisah, sedangkan konstruksi yang didukung bergantung pada implementasi dan versi protokol yang digunakan.

Peran server dalam pertukaran prekey tidak mengharuskan server memiliki private key pasangannya. Private identity key, private material untuk signed prekey dan one-time prekey, serta secret sesi aktif tetap berada di endpoint.

Double Ratchet mengganti key seiring pertukaran pesan

Setelah dua endpoint memiliki shared secret material, algoritma Double Ratchet menurunkan key baru selama percakapan berlangsung.

State-nya menggabungkan symmetric-key chain dengan langkah Diffie-Hellman ratchet. Message key diturunkan dari chain key, lalu chain bergerak maju alih-alih mengenkripsi setiap pesan berulang kali menggunakan satu symmetric key berumur panjang. Output Diffie-Hellman secara berkala dicampurkan ke evolusi root key ketika kedua pihak bertukar ratchet public key baru.

Gambaran sederhananya:

root key
   |
   +--> sending chain key --> message key 0
   |          |
   |          +-----------> message key 1
   |          |
   |          +-----------> message key 2
   |
   +--> receiving chain state

new remote ratchet public key
   |
   v
DH ratchet step
   |
   v
new root / chain state

Konsekuensi keamanannya berasal dari evolusi dan penghapusan key. State yang lebih baru seharusnya tidak menyediakan cara langsung untuk merekonstruksi message key lama yang sudah dipakai dan dihapus. Diffie-Hellman ratchet berikutnya juga dapat memasukkan secret material baru sehingga memberikan properti pemulihan setelah bentuk kompromi state tertentu, setelah input ratchet yang tidak terkompromi dipertukarkan.

Properti tersebut bergantung pada pengelolaan state yang benar. Menyimpan seluruh historical message key selamanya menghilangkan sebagian manfaat penghapusan key yang sudah tidak diperlukan.

Delivery tidak berurutan membutuhkan skipped message key

Delivery jaringan tidak dijamin selalu mengikuti urutan percakapan. Perangkat dapat menerima pesan 12 sebelum pesan 10 karena retry, multi-path delivery, disconnect sementara, atau perilaku antrean.

Spesifikasi Double Ratchet menangani kondisi ini dengan memungkinkan receiver menurunkan dan menyimpan sementara skipped message key ketika receiving chain bergerak maju. Ketika pesan yang terlambat tiba, receiver dapat memakai key tersimpan untuk message number tersebut.

diterima:  8, 9, 12

derive key 10 -> simpan sementara
derive key 11 -> simpan sementara
derive key 12 -> decrypt message 12

kemudian:
message 10 -> gunakan dan hapus stored key 10
message 11 -> gunakan dan hapus stored key 11

State ini tidak boleh tumbuh tanpa batas. Spesifikasi menetapkan jumlah maksimum skipped message key yang bersedia disimpan implementasi. Tanpa batas tersebut, attacker dapat mengirim pesan yang mengklaim counter sangat jauh dan memaksa konsumsi komputasi atau storage berlebihan.

Implementasi Go tidak seharusnya mengganti mekanisme ini dengan map tanpa batas hanya karena map mudah digunakan. Batas skipped key merupakan bagian dari resource-safety boundary protokol.

Persistence harus atomik terhadap ratchet advancement

Ratchet state bersifat mutable. Enkripsi atau dekripsi sebuah pesan dapat menaikkan counter, mengganti chain key, menambah skipped key, memakai lalu menghapus skipped key, atau menjalankan Diffie-Hellman ratchet.

Karena itu, persistence menjadi persoalan correctness sekaligus storage.

Pertimbangkan jalur outbound:

load session
    |
derive message key
    |
advance sending chain
    |
encrypt message
    |
persist new session state
    |
enqueue ciphertext

Jika proses crash di antara mutasi state dan durable storage, perilaku retry bergantung pada protocol library dan transaction boundary aplikasi. Menggunakan ulang cryptographic state yang seharusnya sudah bergerak maju dapat berbahaya; memajukan state tanpa mempertahankan ciphertext juga dapat menimbulkan celah operasional.

Aplikasi memerlukan atomic boundary yang didefinisikan dengan jelas di sekitar update protocol state dan operasi message queue. Strategi transaksi persisnya bergantung pada library dan arsitektur storage, tetapi “simpan nanti” bukan aturan generik yang aman untuk ratcheting state.

Masalah yang sama muncul pada receive path. Pesan yang berhasil diautentikasi dapat memakai skipped key atau memajukan receiving chain. Memproses ulang envelope yang sama terhadap state lama tidak identik dengan memprosesnya satu kali.

Concurrency Go dapat merusak sesi yang secara logis serial

Go memudahkan pemrosesan pesan secara concurrent. Cryptographic session tidak otomatis aman untuk dimutasi secara concurrent.

Dua goroutine yang membaca snapshot sesi yang sama dapat menurunkan state dari counter yang sama sebelum salah satunya menyimpan update:

goroutine A                    goroutine B
-----------                    -----------
load state N                   load state N
derive next key                derive next key
advance to N+1                 advance to N+1
save state                     save state

Mutex dalam satu process dapat mencegah race lokal, tetapi tidak menyelesaikan mutasi concurrent pada beberapa process atau replica. Distributed service membutuhkan serialization boundary yang lebih kuat, misalnya transactional compare-and-swap, row locking, actor-style owner untuk setiap sesi, atau mekanisme lain yang kompatibel dengan storage yang dipilih.

Invariant yang perlu dijaga lebih sempit daripada “buat database thread-safe”: update terhadap satu cryptographic session harus mengamati urutan yang koheren.

Ini juga alasan untuk tidak menaruh endpoint Signal session begitu saja di relay yang di-scale secara horizontal. Jika relay hanya merutekan ciphertext, relay sama sekali tidak perlu memutasi sesi endpoint tersebut.

Verifikasi identitas terpisah dari kerahasiaan ciphertext

Dekripsi yang berhasil membuktikan bahwa pesan valid terhadap key yang terkait dengan session state saat ini. Hal itu sendiri tidak memberi tahu manusia bahwa remote identity key benar-benar milik orang yang ingin mereka hubungi.

Sistem bergaya Signal menyediakan mekanisme identity verification agar user atau perangkat dapat membandingkan informasi identitas terautentikasi melalui konteks tepercaya lain. Aplikasi yang mengabaikan perbedaan ini dapat menyediakan komunikasi terenkripsi sambil tetap menyisakan initial key substitution sebagai persoalan trust terpisah.

Server juga dapat memengaruhi public prekey material yang diterima client. Spesifikasi protokol memperhitungkan trust boundary ini, tetapi aplikasi tetap membutuhkan kebijakan untuk identity change: menerima otomatis, memberi peringatan, memblokir sampai ada konfirmasi, atau menerapkan trust model khusus produk.

Kebijakan tersebut berada di atas implementasi ratchet mentah.

Surface libsignal resmi bukan SDK Go

Repository libsignal Signal saat ini mengimplementasikan fungsi inti dalam Rust dan mengekspos API yang digunakan Signal untuk Java, Swift, dan TypeScript. Repository tersebut secara eksplisit menyatakan bahwa penggunaan di luar Signal tidak didukung dan bridge API dapat berubah.

Tidak ada surface API Go resmi yang setara di repository tersebut.

Konsekuensinya bersifat arsitektural. Aplikasi Go memiliki beberapa pilihan yang sangat berbeda: memelihara implementasi native yang diaudit dengan cermat, melakukan binding ke implementasi lain melalui boundary yang stabil, mengisolasi operasi kriptografi dalam service yang menggunakan surface library yang didukung, atau memakai implementasi Go pihak ketiga setelah mengevaluasi versi protokol, status maintenance, interoperability, persistence semantics, dan security review.

Pilihan tersebut tidak setara. Package yang memakai nama Signal Protocol dapat mengimplementasikan generasi protokol yang lebih lama dan tetap dapat dikompilasi tanpa masalah.

Untuk deployment yang sensitif terhadap keamanan, kompatibilitas protokol sebaiknya dibuktikan dengan test vector dan cross-implementation test, bukan disimpulkan dari nama package.

Relay sebaiknya tetap sederhana

Desain server-side yang bersih memberikan otoritas kriptografi sesedikit mungkin kepada relay Go:

perangkat pengirim
  plaintext
     |
     v
Signal session
     |
     | ciphertext envelope
     v
Go relay
  - authenticate account/device
  - route envelope
  - queue while recipient is offline
  - enforce size/rate policy
     |
     | ciphertext envelope
     v
perangkat penerima
     |
     v
Signal session
     |
     v
  plaintext

TLS tetap diperlukan antara perangkat dan relay. TLS melindungi connection metadata dan application traffic dari network observer biasa, mengautentikasi server berdasarkan model TLS aplikasi, dan mencegah tampering pada level transport. TLS hanya melindungi boundary yang berbeda dari end-to-end message encryption.

Pilihan WebSocket atau HTTP juga berubah menjadi keputusan operasional, bukan keputusan kriptografi. Sistem dapat mengganti transport tanpa mendesain ulang Signal session selama encrypted envelope dan delivery semantics tetap mempertahankan kebutuhan protocol layer.

State yang sulit berada di endpoint: identity key, prekey, active ratchet, skipped message key, serta aturan untuk mengubah semuanya tanpa reuse atau race. Memisahkan state tersebut dari networking layer Go memungkinkan transport melakukan reconnect, retry, scaling, bahkan berganti protokol tanpa diam-diam mengubah end-to-end trust boundary.