Debug lisensi DRM sering terasa menipu ketika gejalanya terlihat seperti masalah autentikasi atau kunci tanda tangan, padahal akar masalahnya ada di level yang lebih rendah: parser biner membaca payload dengan asumsi yang salah. Akibatnya, request yang sebenarnya valid bisa berakhir sebagai 403 signature invalid, sementara request lain memicu 500 internal error karena offset parser bergeser dan validasi berikutnya membaca field yang salah.

Artikel ini membahas studi kasus backend lisensi DRM/internal entitlement yang gagal memproses request karena validasi data biner yang keliru. Fokusnya bukan pada bypass DRM, melainkan pada pelajaran defensif untuk sistem backend yang menerima payload biner atau protobuf serupa: bagaimana gejalanya muncul, mengapa log sering menyesatkan, cara mereproduksi bug secara aman, dan bagaimana memperbaiki parser serta observability agar kasus yang sama tidak terulang.

Gejala: 403 dan 500 Acak, Padahal Kunci Tidak Berubah

Insiden biasanya dimulai dari pola yang membingungkan:

  • Sebagian request lisensi sukses, sebagian lain gagal dengan 403.
  • Beberapa request identik secara bisnis justru berakhir 500.
  • Verifikasi signature atau hash gagal, tetapi hanya pada subset payload tertentu.
  • Log menampilkan pesan seperti invalid token, bad signature, atau unexpected end of input tanpa konteks byte offset.

Masalah ini umum pada backend yang memproses token entitlement atau challenge/response berbentuk biner. Selama formatnya sederhana, bug parser mungkin tersembunyi. Begitu ada variasi payload—misalnya field opsional muncul, panjang field berubah, atau framing tidak persis seperti asumsi awal—bug yang tadinya laten mulai terlihat sebagai error acak.

Mengapa 403 dan 500 Bisa Muncul Bersamaan?

Dua jenis error ini biasanya berasal dari dua tahap berbeda:

  1. 403 muncul ketika parser masih berhasil membaca cukup banyak data untuk masuk ke tahap verifikasi, tetapi isi field sudah salah karena offset bergeser. Signature, nonce, atau message digest lalu dihitung terhadap byte yang keliru.
  2. 500 muncul ketika pergeseran offset lebih parah, misalnya panjang field dibaca terlalu besar sehingga parser mencoba membaca di luar boundary buffer, memicu exception atau panic.

Itulah sebabnya gejala tampak acak. Bukan karena backend nondeterministik, melainkan karena variasi payload menentukan seberapa jauh parser salah membaca data sebelum akhirnya gagal.

Studi Kasus: Salah Asumsi pada Format Data Biner

Dalam kasus ini, layanan entitlement menerima request biner yang secara garis besar terdiri dari:

  • header kecil untuk versi/protocol marker,
  • field panjang payload,
  • blob metadata/token,
  • signature atau MAC di bagian akhir.

Bug muncul karena implementasi backend mengasumsikan bahwa field panjang selalu memakai representasi tetap 4 byte big-endian. Kenyataannya, sebagian client atau upstream mengirimkan field panjang dalam bentuk length-delimited yang tidak selalu 4 byte tetap, atau membungkus pesan dengan framing tambahan. Selama nilainya kecil, bug tidak selalu terlihat. Ketika ukuran metadata bertambah atau field opsional hadir, pembacaan bergeser.

Contoh anti-pattern yang sering terjadi:

// asumsi salah: 4 byte berikutnya selalu panjang payload metadata
func parseRequest(buf []byte) ([]byte, []byte, error) {
    if len(buf) < 8 {
        return nil, nil, errors.New("buffer too short")
    }

    // header 4 byte
    pos := 4

    // SALAH jika format riil bukan fixed-width uint32
    payloadLen := binary.BigEndian.Uint32(buf[pos : pos+4])
    pos += 4

    end := pos + int(payloadLen)
    payload := buf[pos:end]
    sig := buf[end:]
    return payload, sig, nil
}

Jika framing sebenarnya menggunakan skema panjang yang bervariasi atau field lain di antara header dan payload, maka:

  • payloadLen akan salah,
  • payload memotong byte yang bukan miliknya,
  • sig dimulai pada offset yang keliru,
  • verifikasi kriptografi gagal walaupun kunci benar.

Kasus seperti ini sering muncul saat engineer menyederhanakan parser berdasarkan satu sampel payload atau dokumentasi tidak lengkap. Inspirasi praktis dari riset parser/protokol DRM menunjukkan satu pelajaran penting: format biner jarang aman untuk ditebak dari sedikit contoh. Boundary, framing, dan tipe field harus diverifikasi eksplisit.

Mengapa Log Awal Menyesatkan

Pada banyak backend, log dibuat di lapisan bisnis:

  • signature verification failed
  • token not authorized
  • entitlement rejected

Log ini tidak salah, tetapi tidak menjelaskan akar masalah. Verifikasi memang gagal, hanya saja penyebabnya bukan kredensial atau token palsu, melainkan parser yang salah membaca byte.

Beberapa pola log yang sering menyesatkan:

  • Hash mismatch dianggap masalah rotasi secret.
  • Invalid protobuf dianggap payload rusak di jaringan, padahal parser memotong framing secara salah sebelum deserialisasi.
  • Unauthorized dianggap issue IAM atau ACL, padahal field subject/client_id salah parse.

Karena itu, saat melakukan debug lisensi DRM, jangan berhenti di error business logic. Validasi dulu apakah byte yang masuk ke tahap verifikasi memang byte yang benar.

Cara Reproduksi Lokal dengan Aman

Target reproduksi lokal bukan untuk membongkar DRM, melainkan untuk memastikan parser backend membaca framing dan boundary secara konsisten. Cara paling aman adalah menangkap payload dari lingkungan uji atau fixture internal yang sudah disanitasi, lalu memutarnya ulang ke parser lokal.

1. Simpan Sampel Payload Mentah Secara Terkontrol

Jangan log seluruh payload produksi ke sistem log umum. Simpan fixture dengan aturan berikut:

  • hapus atau mask identifier sensitif,
  • jangan simpan kunci privat atau signature secret,
  • simpan hanya byte request yang diperlukan untuk reproduksi parser,
  • beri label versi protokol dan sumber client.

Format penyimpanan yang praktis:

{
  "case": "payload_with_optional_field",
  "content_type": "application/octet-stream",
  "body_base64": "AAECAwQF...",
  "expected": {
    "parse_ok": true,
    "message_length": 173
  }
}

2. Buat Harness Parser Offline

Jalankan parser tanpa jaringan, database, dan tanpa verifikasi kriptografi dulu. Tujuannya memisahkan bug parsing dari noise sistem lain.

func main() {
    raw, err := os.ReadFile("fixtures/request.bin")
    if err != nil {
        log.Fatal(err)
    }

    msg, sig, err := parseRequest(raw)
    if err != nil {
        log.Fatalf("parse failed: %v", err)
    }

    fmt.Printf("payload=%d bytes signature=%d bytes\n", len(msg), len(sig))
    fmt.Printf("payload_prefix=%x\n", msg[:min(16, len(msg))])
}

Jika parser lokal sudah gagal sebelum verifikasi, akar masalah hampir pasti ada pada framing atau boundary.

3. Uji Beberapa Variasi Panjang

Bug semacam ini sering hanya muncul pada ukuran tertentu. Karena itu, buat fixture dengan:

  • payload sangat kecil,
  • payload tepat di sekitar boundary tertentu,
  • payload yang mengandung field opsional,
  • payload dari beberapa implementasi client.

Jika hanya ada satu sampel, parser yang salah bisa tampak benar.

Teknik Tracing Payload yang Aman

Saat backend memproses format biner, observability perlu cukup detail untuk debugging tetapi tetap aman. Hindari mencetak seluruh payload ke log teks biasa. Lebih baik catat metadata parsing yang bisa ditindaklanjuti.

Apa yang Perlu Dicatat

  • ukuran buffer total,
  • offset parser per tahap,
  • panjang field hasil decode,
  • jenis framing atau versi protokol yang terdeteksi,
  • hash singkat payload mentah untuk korelasi, misalnya SHA-256 yang dipotong.

Contoh log terstruktur yang lebih berguna daripada sekadar signature invalid:

{
  "event": "license_request_parse",
  "request_id": "req-7f3a",
  "buffer_len": 241,
  "header_version": 2,
  "length_field_encoding": "varint",
  "decoded_payload_len": 173,
  "payload_start": 6,
  "payload_end": 179,
  "signature_start": 179,
  "raw_sha256_prefix": "a91c3f12",
  "parse_error": null
}

Catat offset dan panjang, bukan isi payload lengkap. Ini jauh lebih aman dan jauh lebih berguna untuk menemukan mismatch parser.

Hex Dump Terbatas Lebih Baik daripada Full Dump

Jika benar-benar perlu melihat byte mentah, gunakan dump terbatas di sekitar boundary yang bermasalah, misalnya 16–32 byte sebelum dan sesudah offset. Fokus pada titik kegagalan, bukan seluruh pesan.

func hexWindow(buf []byte, center, radius int) string {
    start := center - radius
    if start < 0 {
        start = 0
    }
    end := center + radius
    if end > len(buf) {
        end = len(buf)
    }
    return hex.EncodeToString(buf[start:end])
}

Pendekatan ini membantu melihat apakah boundary salah satu atau dua byte, tanpa membuka seluruh data sensitif.

Root Cause: Boundary Check Ada, tapi Validasi Semantik Tidak Ada

Setelah dianalisis, masalah utama biasanya bukan sekadar kurang if len(buf). Boundary check dasar memang ada, tetapi parser tidak memvalidasi bahwa struktur yang dibaca masuk akal secara semantik.

Contohnya:

  • panjang payload lolos karena masih lebih kecil dari ukuran buffer total,
  • tetapi parser tidak mengecek bahwa sisa byte cukup untuk signature minimum,
  • parser tidak mengecek bahwa field versi cocok dengan framing yang dipakai,
  • parser langsung deserialisasi protobuf tanpa memastikan ia mulai dari offset yang benar.

Dengan kata lain, parser aman dari crash sederhana, tetapi belum aman dari misparse. Ini lebih berbahaya karena menghasilkan output salah yang terlihat valid sampai tahap lebih lanjut.

Contoh Perbaikan Parser

Perbaikan pentingnya adalah memisahkan beberapa tahap dengan validasi eksplisit:

  1. validasi header minimum,
  2. deteksi framing berdasarkan marker/versi,
  3. decode panjang field sesuai encoding yang benar,
  4. cek boundary untuk payload dan signature,
  5. cek invariants semantik sebelum lanjut ke verifikasi.
type ParsedRequest struct {
    Version   byte
    Payload   []byte
    Signature []byte
}

func parseRequest(buf []byte) (*ParsedRequest, error) {
    if len(buf) < 3 {
        return nil, fmt.Errorf("short buffer: %d", len(buf))
    }

    version := buf[0]
    flags := buf[1]
    _ = flags
    pos := 2

    payloadLen, n, err := decodeLength(buf[pos:])
    if err != nil {
        return nil, fmt.Errorf("decode length at %d: %w", pos, err)
    }
    pos += n

    if payloadLen < 0 {
        return nil, fmt.Errorf("negative payload length")
    }

    if pos+payloadLen > len(buf) {
        return nil, fmt.Errorf("payload out of bounds: pos=%d len=%d total=%d", pos, payloadLen, len(buf))
    }

    payload := buf[pos : pos+payloadLen]
    pos += payloadLen

    if len(buf)-pos < minSignatureLen {
        return nil, fmt.Errorf("signature too short: remaining=%d", len(buf)-pos)
    }

    sig := buf[pos:]

    if !looksLikeExpectedMessage(payload) {
        return nil, fmt.Errorf("payload semantic validation failed")
    }

    return &ParsedRequest{
        Version:   version,
        Payload:   payload,
        Signature: sig,
    }, nil
}

Poin penting di sini:

  • decodeLength harus mengikuti encoding riil, bukan asumsi sementara.
  • Validasi bukan hanya out of bounds, tetapi juga apakah struktur ini masuk akal untuk protokol ini.
  • Error menyertakan offset dan ukuran agar mudah ditelusuri.

Memastikan Verifikasi Hash/Signature Dilakukan di Byte yang Benar

Dalam banyak sistem entitlement, signature atau MAC dihitung dari representasi byte yang spesifik. Jika parser melakukan normalisasi, trimming, atau re-encoding sebelum verifikasi, hasilnya bisa salah walaupun field semantik sama.

Prinsip aman:

  • verifikasi signature terhadap canonical byte sequence yang memang ditentukan protokol,
  • jangan membangun ulang payload untuk diverifikasi jika protokol mengharuskan byte mentah asli,
  • bedakan antara byte framing luar dan byte message inti yang ditandatangani,
  • dokumentasikan dengan jelas bagian mana yang menjadi input verifikasi.

Salah satu bug umum adalah parser berhasil membentuk objek message, lalu verifikasi dilakukan pada hasil serialisasi ulang objek tersebut. Untuk format biner yang punya beberapa kemungkinan representasi wire-level, pendekatan ini bisa salah.

Test Regresi yang Wajib Ditambahkan

Begitu root cause ditemukan, jangan cukup berhenti pada patch parser. Tambahkan test yang mengunci perilaku agar bug serupa tidak kembali saat ada refactor.

1. Golden Test untuk Sampel Nyata

Simpan sejumlah fixture biner yang mewakili kasus valid dan invalid.

func TestParseRequestFixtures(t *testing.T) {
    cases := []struct {
        name      string
        file      string
        shouldErr bool
    }{
        {"valid_basic", "testdata/valid_basic.bin", false},
        {"valid_optional_field", "testdata/valid_optional.bin", false},
        {"invalid_truncated", "testdata/invalid_truncated.bin", true},
        {"invalid_length_mismatch", "testdata/invalid_len.bin", true},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            raw, err := os.ReadFile(tc.file)
            if err != nil {
                t.Fatal(err)
            }
            _, err = parseRequest(raw)
            if tc.shouldErr && err == nil {
                t.Fatal("expected error")
            }
            if !tc.shouldErr && err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
        })
    }
}

2. Boundary Test

Uji panjang tepat di sekitar batas yang rawan:

  • 0 byte,
  • 1 byte,
  • batas minimum signature,
  • panjang payload maksimum yang diizinkan backend,
  • payload yang berakhir tepat di ujung buffer.

3. Fuzzing Parser

Untuk parser biner, fuzzing sangat efektif menemukan mismatch boundary dan panic. Tujuannya bukan memastikan semua input diterima, melainkan memastikan input acak tidak menyebabkan crash, loop tak berujung, atau hasil parse yang tidak konsisten.

Kriteria minimum fuzzing:

  • parser tidak panic,
  • error terkontrol dan informatif,
  • jika parse sukses, invariants internal selalu benar.

Guardrail Observability Setelah Perbaikan

Setelah parser diperbaiki, tambahkan guardrail agar insiden serupa terlihat lebih cepat di masa depan.

Metrik yang Perlu Dipantau

  • rasio parse error per client/version,
  • rasio signature failure setelah parse sukses,
  • distribusi ukuran payload,
  • jumlah request dengan length mismatch,
  • jumlah request yang gagal pada offset parsing tertentu.

Pemisahan metrik ini penting. Jika semua digabung sebagai license request failed, Anda tidak tahu apakah masalah ada di parser, verifikasi, atau policy.

Structured Error Classification

Gunakan kode error internal yang stabil, misalnya:

  • PARSE_SHORT_BUFFER
  • PARSE_LENGTH_DECODE_FAILED
  • PARSE_LENGTH_OUT_OF_BOUNDS
  • PARSE_SEMANTIC_INVALID
  • SIGNATURE_INVALID

Dengan klasifikasi ini, dashboard dan alert dapat membedakan lonjakan parse error dari lonjakan signature error. Itu sangat membantu saat rollout client baru atau perubahan upstream framing.

Sampling Log untuk Kasus Langka

Jika traffic tinggi, jangan log detail semua request. Terapkan sampling pada error parser, misalnya hanya sebagian kecil contoh per kategori, tetapi tetap hitung metrik agregat penuh. Ini menjaga biaya dan risiko eksposur data tetap rendah.

Pelajaran Utama untuk Backend yang Menangani Format Biner/Protobuf

Kasus ini memberi beberapa pelajaran praktis yang berlaku luas, tidak hanya untuk layanan lisensi DRM:

  • Jangan menebak wire format dari sedikit contoh. Dokumentasi framing, panjang field, dan canonical bytes harus jelas.
  • Boundary check saja tidak cukup. Tambahkan validasi semantik agar parser tidak menerima struktur yang salah tetapi masih tampak valid.
  • Pisahkan tahap parse dari tahap verify. Dengan begitu, 403 dan 500 bisa ditelusuri ke lapisan yang benar.
  • Log offset dan ukuran, bukan payload penuh. Ini lebih aman dan lebih berguna untuk debugging.
  • Tambahkan fixture nyata dan fuzzing. Parser biner adalah tempat yang sangat cocok untuk regresi halus.

Dalam debug lisensi DRM, error kriptografi sering menjadi kambing hitam pertama. Padahal, jika parser salah membaca satu field panjang saja, seluruh rantai validasi di belakangnya ikut gagal. Karena itu, saat melihat 403 atau 500 acak pada backend entitlement, mulailah dari satu pertanyaan mendasar: apakah kita benar-benar memverifikasi byte yang memang dikirim client?