Error Format Kunci Privat RS256: Satu Pesan, Tujuh Penyebab
Error format kunci privat RS256 hampir tidak pernah menyebut penyebabnya sendiri. Di Node v25.8.2, setiap kesalahan berikut menghasilkan baris yang persis sama:
code: ERR_OSSL_UNSUPPORTED
message: error:1E08010C:DECODER routines::unsupported
Lima hal yang sama sekali tidak berhubungan bisa memicunya: kontainer (container) OpenSSH padahal yang diharapkan kunci PEM, baris -----BEGIN yang terindentasi, kunci publik yang diserahkan ke penanda tangan, urutan \n literal yang tidak pernah di-unescape siapa pun, dan berkas yang baris barunya hilang di perjalanan. Daftar hasil pengujian di Bagian 2 bahkan mencapai tujuh butir. Node memakai susunan kata yang sama persis setiap kali, dan itulah sebabnya mencari teks error tersebut selalu menjatuhkan Anda ke thread orang lain tentang penyebab yang lain lagi.
Pertama, belah dulu masalahnya jadi dua:
- Pustaka (library) tidak pernah mendapatkan objek kunci. Tetap di sini.
- Pustaka berhasil memuat kunci, lalu berkata
invalid signature. Itu kegagalan lain dengan penyebab lain, dan pembahasannya ada di JWT invalid signature: Semua Penyebab dan Cara Memperbaikinya.
Triase tiga puluh detik untuk kasus pertama:
openssl rsa -in key.pem -noout -text | head -1
Kalau perintah itu error, berkasnyalah yang bermasalah dan Bagian 3 sampai 6 akan menemukannya. Kalau berhasil, OpenSSL memahami kontainernya, dan masalah Anda ada di sisi pustaka atau di apa yang Anda serahkan kepadanya, yaitu Bagian 4 dan 7.
Semua yang ada di bawah ini diukur pada 2026-08-11 terhadap OpenSSL 3.6.2 7 Apr 2026, Node v25.8.2, Go go1.26.1 darwin/arm64, dan Java 1.8.0_162. Kalau sebuah klaim berasal dari membaca kode sumber alih-alih menjalankannya, teksnya akan menyebutkan hal itu.
1. Pertama, tentukan jenis kegagalan yang Anda hadapi
Garis pemisahnya adalah apakah objek kunci pernah terbentuk atau tidak. Kegagalan parse-time terjadi sebelum kriptografi apa pun berjalan. Pustaka membaca PEM Anda, gagal mengubahnya menjadi kunci, lalu melempar error. Tidak ada yang sempat ditandatangani, dan token yang sedang Anda debug memang tidak pernah dibuat. Kegagalan verify-time justru kebalikannya: kunci termuat dengan bersih, pustaka menghitung tanda tangan, dan hasilnya tidak cocok. Yang seperti itu berasal dari ketidaksepakatan pada level byte antara penanda tangan dan verifier, dan panduan invalid signature membahasnya.
Membedakan keduanya cukup dengan sekali lihat ke stack trace. Kegagalan parse-time menyebut decoder, key spec, atau struktur ASN.1. Kegagalan verify-time menyebut tanda tangan.
Berikut pesan yang muncul saat ketiga ekosistem menolak sebuah kunci privat:
| Runtime | Versi yang diuji | Pesan ketika kunci gagal dimuat |
|---|---|---|
Node crypto | v25.8.2 | error:1E08010C:DECODER routines::unsupported |
Go crypto/x509 | go1.26.1 darwin/arm64 | x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) |
Java PKCS8EncodedKeySpec | 1.8.0_162 | InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence |
Tingkat kegunaan ketiganya jauh berbeda. Go memberi tahu persis fungsi mana yang seharusnya Anda panggil. Java menyebut “algid” dan “sequence”, lalu membiarkan Anda menyimpulkan sendiri bahwa maksudnya kunci Anda ada di kontainer yang salah. Node sama sekali tidak mengatakan apa pun yang bisa dipakai.
Kalau Anda belum yakin token yang sedang dikejar itu benar-benar RS256, tempelkan ke Dekode JWT Online lalu baca alg dari header sebelum melangkah lebih jauh. Header HS256 berarti yang Anda butuhkan adalah secret bersama, bukan pasangan kunci, dan setiap gejala dalam artikel ini akan menuntun Anda ke arah yang keliru.
2. Dari teks error ke akar masalah: tabel pencarian error format kunci privat RS256
Cari string persis milik Anda. Kolom kanan adalah tujuan berikutnya.
| Teks error | Asalnya | Apa artinya sebenarnya |
|---|---|---|
error:1E08010C:DECODER routines::unsupported | Node v25.8.2 | Tujuh kemungkinan penyebab, tercantum di bawah |
error:07880109:common libcrypto routines::interrupted or cancelled | Node v25.8.2 | Kunci terenkripsi dan Anda tidak memberikan passphrase |
x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format) | Go 1.26.1 | Anda memanggil ParsePKCS1PrivateKey pada berkas PKCS#8 |
x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format) | Go 1.26.1 | Anda memanggil ParsePKCS8PrivateKey pada berkas PKCS#1 |
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...}) | Go 1.26.1 | Blok PEM pertama adalah EC PARAMETERS, bukan kuncinya |
algid parse error, not a sequence | Java 1.8.0_162 | PKCS#1 diserahkan ke PKCS8EncodedKeySpec |
secretOrPrivateKey must have a value | jsonwebtoken, di kode sumber | Argumen kunci bernilai falsy dan alg bukan none |
secretOrPrivateKey is not valid key material | jsonwebtoken, di kode sumber | Tidak ada kunci privat maupun secret key yang bisa dibangun |
secretOrPrivateKey must be a symmetric key when using ${header.alg} | jsonwebtoken, di kode sumber | alg diawali HS tetapi kuncinya bukan secret |
secretOrPrivateKey must be an asymmetric key when using ${header.alg} | jsonwebtoken, di kode sumber | alg cocok dengan RS, PS, atau ES tetapi kuncinya bukan kunci privat |
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg} | jsonwebtoken, di kode sumber | RS atau PS dengan kunci di bawah 2048 bit dan allowInsecureKeySizes tidak aktif |
Kelima string secretOrPrivateKey itu dibaca dari sign.js di branch master jsonwebtoken, bukan dijalankan secara lokal, jadi perlakukan kondisi pemicunya sebagai apa yang tertulis di kode sumber, bukan sebagai sesuatu yang direproduksi di mesin ini. Bagian ${header.alg} adalah placeholder template di kode sumber tersebut; saat runtime yang muncul di sana adalah nama algoritma Anda sendiri, dan itulah sebabnya mencari string literal lengkap dengan kurung kurawalnya tidak menemukan apa pun.
Tujuh cara menghasilkan DECODER routines::unsupported
Ketujuhnya direproduksi terhadap crypto.createPrivateKey() di Node v25.8.2, dan ketujuhnya memberikan kode serta pesan yang sama:
- Kontainer OpenSSH. Berkasnya diawali
-----BEGIN OPENSSH PRIVATE KEY-----dan sama sekali bukan struktur kunci PEM. - Baris
-----BEGINyang terindentasi, atau baris-----ENDyang terindentasi. Baris isi dikecualikan; Bagian 5 memuat batas persisnya. - Spasi di depan keseluruhan PEM. Satu baris kosong di depan tidak masalah, satu spasi di depan bermasalah.
- Baris baru dihapus seluruhnya, sehingga header, base64, dan footer menyatu dalam satu baris.
- Kunci publik diberikan di tempat yang mengharapkan kunci privat.
- Urutan backslash-n literal yang dibiarkan tanpa di-unescape, yaitu wujud yang dihasilkan environment variable satu baris.
- Pembatas dengan jumlah tanda hubung yang keliru, atau
begin/endyang ditulis dengan huruf kecil.
Dua di antaranya masalah kontainer, empat masalah teks yang rusak, dan satu murni salah ambil berkas. Pesannya tidak bisa memberi tahu yang mana, jadi cara tercepat adalah mengeliminasi satu per satu alih-alih menafsirkan teksnya.
Apa yang diterima Node, dan kenapa itu mempersempit pencarian lebih cepat
Daftar kebalikannya justru lebih berguna, karena setiap butirnya adalah teori yang bisa langsung Anda coret. Di Node v25.8.2, crypto.createPrivateKey() menerima semua hal berikut tanpa protes:
- Kunci privat PKCS#1 dan PKCS#8
- Kunci privat EC SEC1
- Akhiran baris CRLF
- Baris baru terakhir yang hilang
- Isi base64 dalam satu baris tanpa pelipatan
- Baris isi yang terindentasi
- Satu baris kosong sebelum PEM
- BOM UTF-8, baik dalam bentuk
'' + pemmaupun sebagaiBufferyang diawali0xEF 0xBB 0xBF - Header PKCS#1 yang dibungkuskan pada isi PKCS#8
Butir terakhir punya konsekuensi langsung. Decoder membaca struktur DER di dalam base64 dan mengabaikan label di luarnya, sehingga berkas bertuliskan BEGIN RSA PRIVATE KEY dengan isi PKCS#8 tetap termuat. Fakta ini berguna, sekaligus jadi peringatan untuk Bagian 3: baris header itu petunjuk, bukan jaminan.
3. Baris header PEM: kontainer mana yang sebenarnya Anda pegang
Setiap PEM memperkenalkan dirinya di baris pertama. Berikut nilai header yang ditulis oleh OpenSSL 3.6.2:
| Isi | Baris pertama |
|---|---|
| Kunci privat PKCS#8 | -----BEGIN PRIVATE KEY----- |
| Kunci privat PKCS#1 | -----BEGIN RSA PRIVATE KEY----- |
| Kunci privat terenkripsi | -----BEGIN ENCRYPTED PRIVATE KEY----- |
| Kunci privat OpenSSH | -----BEGIN OPENSSH PRIVATE KEY----- |
| Kunci privat EC SEC1 | -----BEGIN EC PARAMETERS-----, lalu blok kedua -----BEGIN EC PRIVATE KEY----- |
| Kunci publik SPKI | -----BEGIN PUBLIC KEY----- |
| Kunci publik PKCS#1 | -----BEGIN RSA PUBLIC KEY----- |
| Kunci privat Ed25519 | -----BEGIN PRIVATE KEY-----, dan seluruh berkasnya hanya tiga baris |
Jadi head -1 key.pem menjawab pertanyaan pertama dari investigasi mana pun. Tiga baris dalam tabel itu perlu keterangan tambahan.
ENCRYPTED PRIVATE KEY bukan error format. Itu passphrase yang lupa Anda berikan. Node melaporkannya berbeda dari yang lain, dengan ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED dan error:07880109:common libcrypto routines::interrupted or cancelled, karena pustaka meminta passphrase dan tidak mendapat balasan. Jangan campur pesan ini dengan pesan DECODER; keduanya tidak ada hubungannya sama sekali.
OPENSSH PRIVATE KEY adalah dunia yang berbeda. OpenSSH menulis kontainernya sendiri, yang bukan PKCS#1 maupun PKCS#8 meski duduk di antara pembatas yang tampak seperti PEM. Node menolaknya mentah-mentah, begitu pula parser crypto/x509 milik Go dan PKCS8EncodedKeySpec milik JDK. Kalau kunci penanda tangan JWT Anda keluar dari ssh-keygen, di situlah bug-nya.
Berkas EC SEC1 memuat dua blok. openssl ecparam -genkey menulis blok EC PARAMETERS lebih dulu dan kunci privat setelahnya. Apa pun yang hanya membaca blok PEM pertama akan mendapat parameter, lalu gagal dengan cara yang tidak menyinggung keduanya. Bagian 4 memuat versi Go dari kegagalan itu.
Dan karena header hanyalah label, pemeriksaan kebalikannya juga penting: berkas yang header-nya mengatakan satu hal sementara DER-nya mengatakan hal lain akan di-parse mengikuti DER. Membaca head -1 bisa diandalkan untuk berkas yang keluar langsung dari OpenSSL, dan tidak bisa diandalkan untuk berkas yang sempat lewat tangan manusia, halaman wiki, atau skrip yang melakukan penggantian string.
4. Pustaka mana menerima yang mana: PKCS#1 vs PKCS#8 di tiga ekosistem
Sebagian besar perdebatan format antartim berakar pada tabel ini. Setiap barisnya diukur pada versi yang tercantum di awal artikel ini.
| Pustaka | PKCS#1 | PKCS#8 | OpenSSH | Apakah errornya menjelaskan dirinya sendiri? |
|---|---|---|---|---|
Node crypto | Ya | Ya | Tidak | Tidak. Banyak penyebab, satu DECODER routines::unsupported |
Go crypto/x509 | Ya, fungsi khusus | Ya, fungsi khusus | Tidak | Ya. Errornya menyebutkan fungsi penggantinya |
| Pustaka standar Java | Tidak | Ya | Tidak | Tidak. algid parse error, not a sequence justru menyesatkan |
Kolom Java menjelaskan pola kegagalan yang paling sering muncul di tim campuran. Satu layanan Node dan satu layanan Java yang berbagi satu berkas kunci berjalan mulus sampai kuncinya berbentuk PKCS#1; sejak titik itu Node terus menandatangani sementara Java melempar pesan tentang ASN.1 sequence. Tidak ada yang mencurigai kuncinya, karena kunci itu terbukti bekerja di produksi pada layanan satunya.
Node. Tidak ada yang perlu dikonfigurasi. Kalau kontainernya PKCS#1 atau PKCS#8, createPrivateKey() menerimanya. Kalau ia memang melempar error, habiskan waktu Anda pada tujuh penyebab di Bagian 2, bukan pada formatnya.
const fs = require('node:fs');
const { createPrivateKey } = require('node:crypto');
try {
const key = createPrivateKey(fs.readFileSync('key.pem'));
console.log('parsed:', key.asymmetricKeyType);
} catch (err) {
console.log(err.code, '/', err.message);
}
Jalankan itu terhadap berkas yang benar-benar dimuat aplikasi Anda, bukan terhadap salinan yang Anda buat sendiri, dan cabang catch akan mencetak pasangan kode dan pesan yang bisa Anda cari di Bagian 2.
Go. Dua kontainer, dua fungsi, dan memanggil yang keliru adalah kegagalan yang paling sering terjadi di sana. Pesannya memberi tahu mana yang harus dipakai, jadi perbaikannya bersifat mekanis. Mencoba keduanya secara berurutan membuat Anda tidak perlu memilih sejak awal:
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
if err2 != nil {
log.Fatalf("neither container parsed: %v / %v", err, err2)
}
priv = rsaKey
}
Namun jebakan EC perlu Anda tangani sebelum itu. Terhadap berkas dari openssl ecparam -genkey, pem.Decode mengembalikan blok yang Type-nya EC PARAMETERS, dan ketiga fungsi parse gagal padanya dengan:
asn1: structure error: tags don't match (16 vs {class:0 tag:6 ...})
Pesan itu tidak pernah menyinggung blok PEM, jadi reaksi yang biasa muncul adalah mencurigai kuncinya. Lewati saja blok parameter itu:
block, rest := pem.Decode(pemBytes)
if block == nil {
log.Fatal("no PEM block found")
}
if block.Type == "EC PARAMETERS" {
block, _ = pem.Decode(rest)
}
Atau hindari sekalian terbentuknya blok tambahan itu dengan menambahkan -noout pada perintah ecparam yang menulis berkasnya.
Java. Pustaka standarnya membaca PKCS#8 dan tidak yang lain. Berikan kunci PKCS#1 kepada PKCS8EncodedKeySpec di Java 1.8.0_162 dan Anda mendapat:
InvalidKeySpecException: java.security.InvalidKeyException: IOException : algid parse error, not a sequence
“algid” adalah algorithm identifier, field yang ditambahkan PKCS#8 dan tidak dimiliki PKCS#1. Parser mencarinya, menemukan awal sebuah modulus RSA, lalu menyerah. Pesan itu benar dan tidak berguna dalam kadar yang sama. Konversikan berkasnya dan errornya lenyap:
openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem
Jalur pemuatan yang berhasil di Java 8, setelah berkasnya berbentuk PKCS#8, cukup pendek untuk ditempelkan ke dalam sebuah test sambil Anda memastikan perbaikannya:
String pem = new String(Files.readAllBytes(Paths.get("key.pem")), StandardCharsets.UTF_8)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(pem);
PrivateKey key = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(der));
Alternatif selain mengonversi adalah menambahkan BouncyCastle, yang memang membaca PKCS#1. Mengonversi hanya butuh satu perintah dan nol dependensi, jadi pilih konversi kecuali ada bagian lain di stack Anda yang memang sudah membutuhkan pustaka itu.
5. Karakter yang tidak bisa Anda lihat
Nasihat populer tentang spasi dan byte tersembunyi banyak yang keliru, dan setiap butirnya bisa diuji dalam beberapa detik lewat crypto.createPrivateKey().
Indentasi: kebalikan dari yang selama ini Anda dengar
Ada instruksi yang diulang di mana-mana: setiap baris PEM kecuali pembatasnya harus mulai di kolom nol. Diuji di Node v25.8.2, instruksi itu terbalik:
| Perubahan pada berkas | Hasil |
|---|---|
| Semua baris diindentasi | Gagal |
Hanya baris -----BEGIN yang diindentasi | Gagal |
Hanya baris -----END yang diindentasi | Gagal |
| Hanya baris isi base64 yang diindentasi | Diterima |
| Satu spasi sebelum keseluruhan PEM | Gagal |
| Satu baris kosong sebelum keseluruhan PEM | Diterima |
Jadi aturannya adalah: baris -----BEGIN dan -----END harus mulai di kolom nol, sedangkan indentasi pada baris isi tidak berpengaruh. Persis dua baris yang katanya boleh diindentasi justru dua baris yang merusak, dan baris-baris yang katanya harus rata kiri justru yang punya kelonggaran.
Ini penting karena PEM tidak pernah terindentasi dengan sendirinya. Tidak ada yang mengindentasi kunci dengan tangan; indentasi muncul saat seseorang menempelkan kunci ke blok YAML, berkas values Helm, heredoc Terraform, atau string triple-quoted Python di dalam badan class. Semua itu mengindentasi seluruh isinya secara seragam, pembatas ikut terbawa, dan itulah baris pertama pada tabel di atas.
Backslash-n literal dari environment variable satu baris
PEM punya banyak baris, sedangkan environment variable pada praktiknya tidak. Akibatnya kunci mendarat di berkas .env sebagai satu baris dengan \n yang tertulis sebagai dua karakter. Apa pun yang membaca berkas itu menyerahkan string berisi backslash ke kode Anda, dan parser melihat sebuah pembatas yang diikuti sampah. Di Node ini adalah penyebab nomor 6 dari Bagian 2, dengan pesan DECODER routines::unsupported yang sama seperti semua penyebab lainnya.
Kembalikan bentuk aslinya di titik pemakaian:
const pem = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
Ada dua pengaman yang layak ditambahkan di sekitar baris itu. Pertama, terapkan penggantian hanya ketika string-nya memang mengandung urutan dua karakter tersebut, karena nilai yang benar-benar multibaris dan masuk lewat loader lain memang tidak perlu disentuh. Kedua, kalau platform Anda memungkinkan, lebih baik simpan seluruh PEM dalam bentuk base64: satu baris base64, didekode saat startup, dan urusan escaping hilang sepenuhnya.
BOM: tidak berbahaya di Node, dan belum diuji di tempat lain
Byte order mark adalah tiga byte, EF BB BF, yang ditulis sebagian editor Windows di awal berkas UTF-8. Nasihat untuk membuangnya sebelum memuat kunci sangat umum. Di Node v25.8.2 keberadaannya tidak berpengaruh: PEM yang diberi awalan BOM berhasil di-parse, baik sebagai string maupun sebagai Buffer yang diawali ketiga byte itu.
Ruang lingkup hasil itu sempit. Pengujiannya hanya dilakukan di Node v25.8.2. Java, Python, dan parser lain tidak diuji di sini, dan tidak ada satu pun kalimat dalam artikel ini yang menyatakan bagaimana perilaku mereka. Kalau Anda sedang men-debug layanan Java, BOM tetap menjadi pertanyaan terbuka, bukan kemungkinan yang sudah dicoret.
BOM memang merusak hal lain, dan besar kemungkinan dari sanalah nasihat soal kunci itu terbawa lewat asosiasi. JSON.parse pada string berawalan BOM adalah kegagalan nyata yang terdokumentasi dengan baik, dibahas di BOM UTF-8: Perbaiki Error JSON.parse dan CSV Excel. Berkas kunci yang disimpan di dalam konfigurasi JSON karena itu bisa gagal jauh sebelum ada yang melihat kuncinya.
Akhiran baris, baris baru terakhir, dan lebar pelipatan
Tiga tersangka lain yang dibebaskan oleh Node v25.8.2:
- Akhiran baris CRLF diterima. Kunci yang pernah melewati Windows tidak otomatis rusak.
- Baris baru terakhir yang hilang juga diterima, tetapi yang satu ini bergantung pada parser: sebagian parser dikatakan menolak PEM tanpa baris baru penutupnya, dan Node bukan salah satunya. Node menerimanya; parser lain tidak diuji di sini.
- Isi yang tidak dilipat diterima. Base64-nya tidak harus dilipat pada 64 karakter.
Yang benar-benar merusak isi base64 adalah karakter yang hilang, tersisip, atau tergantikan, dan itu kegagalan yang berbeda dari soal pelipatan. Klien chat yang mengubah baris baru menjadi spasi, atau field teks yang memakan satu karakter di ujung, menghasilkan isi yang tidak lagi bisa didekode. Salinlah dengan tombol salin, bukan dengan menyeret mouse.
6. OpenSSL 3.x mengganti nilai default tanpa memberi tahu Anda
Diukur pada OpenSSL 3.6.2 7 Apr 2026:
| Perintah | Kontainer yang ditulisnya |
|---|---|
openssl genrsa -out k.pem 2048 | PKCS#8, header BEGIN PRIVATE KEY |
openssl genrsa -traditional -out k.pem 2048 | PKCS#1 |
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | PKCS#8 |
openssl genpkey -algorithm ED25519 | PKCS#8 |
openssl pkcs8 -topk8 -nocrypt -in a.pem -out b.pem | Mengonversi PKCS#1 menjadi PKCS#8 |
openssl rsa -in b.pem -traditional -out a.pem | Mengonversi PKCS#8 menjadi PKCS#1 |
Dua baris pertama tabel itu yang paling sering mengejutkan orang. genrsa memberi Anda PKCS#8 secara default pada build ini, dan -traditional-lah yang menghasilkan berkas BEGIN RSA PRIVATE KEY. Banyak panduan masih menggambarkan genrsa sebagai perintah PKCS#1 dan genpkey sebagai perintah PKCS#8, dan mengikutinya akan membuat Anda yakin telah menghasilkan format yang sebenarnya tidak Anda hasilkan.
Konsekuensi praktisnya muncul pada migrasi. Sebuah tim Java menerima kunci yang berfungsi dari kolega yang menjalankan OpenSSL versi lama, semuanya baik-baik saja, lalu enam bulan kemudian ada orang yang membuat ulang kunci itu di mesin baru. Perintah sama, dokumentasi sama, kontainer berbeda, dan sekarang JDK melempar algid parse error, not a sequence pada kunci yang “dibuat dengan cara yang persis sama”. Ternyata tidak sama.
Jadi periksa berkasnya sendiri sebelum menebak:
head -1 key.pem
Satu baris keluaran, dan tabel di Bagian 3 memberi tahu apa yang sedang Anda pegang. Lakukan ini sebelum menjalankan perintah konversi apa pun, karena mengonversi berkas PKCS#8 menjadi PKCS#8 adalah operasi kosong yang tampak seperti perbaikan padahal tidak memperbaiki apa-apa.
Kalau Anda lebih suka tidak memusingkan flag sama sekali, Generator Kunci RSA Online mengeluarkan kedua kontainer dari pasangan kunci yang sama lewat satu tombol, sehingga Anda bisa menghasilkan salinan PKCS#1 dan PKCS#8 dari satu kunci lalu mencoba masing-masing pada pustaka yang menolak Anda.
7. Batas bawah 2048 bit yang menolak kunci yang sebenarnya sah
Satu kegagalan di sini terlihat seperti masalah format padahal bukan. Di kode sumber jsonwebtoken, sign.js melempar:
secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}
Kode sumbernya memunculkan pesan itu ketika alg adalah algoritma RS atau PS, kuncinya di bawah 2048 bit, dan allowInsecureKeySizes belum diaktifkan. Pemeriksaan itu milik pustakanya sendiri, bukan milik runtime. Node v25.8.2 mem-parse kunci RSA 1024 bit tanpa protes; modulusLength: 1024 menghasilkan objek kunci seperti objek kunci lainnya. Hasilnya, kuncinya sah secara struktur, kontainernya benar, OpenSSL bisa membacanya, dan pemanggilan penandatanganan tetap gagal.
Penandanya adalah pesan ini menyebut sebuah angka. Error format berbicara tentang decoder, sequence, dan key material; yang ini berbicara tentang bit. Kalau Anda melihat ukuran di dalam pesan, berhentilah menatap PEM.
Asal usul kunci 1024 bit biasanya sejarah: kunci yang dibuat bertahun-tahun lalu mengikuti default yang sejak itu sudah bergeser, atau fixture test yang tidak pernah ditengok lagi karena kunci kecil lebih cepat dibuat. Perbaikannya adalah membuat pasangan baru berukuran 2048 bit atau lebih. Mengaktifkan allowInsecureKeySizes hanya mematikan pemeriksaannya dan meninggalkan kunci lemah itu di tempatnya.
Untuk memastikan ukuran adalah satu-satunya masalah yang tersisa, tandatangani payload yang sama dengan kunci baru berukuran benar di JWT Encoder & Generator. Kalau di sana token berhasil dibuat sementara kode Anda tidak, perbedaannya ada pada kunci Anda, bukan pada klaim atau konfigurasi Anda.
8. Alur kerja yang bisa diulang untuk kegagalan kunci RS256
Jalankan berurutan. Setiap langkah entah menemukan penyebabnya atau memangkas satu cabang.
- Baca baris header.
head -1 key.pem, lalu cocokkan dengan tabel di Bagian 3. Ini memberi tahu kontainernya, apakah berkasnya terenkripsi, dan apakah ia kunci OpenSSH yang tidak akan pernah bisa dipakai. - Minta OpenSSL mem-parse-nya.
openssl rsa -in key.pem -noout -text | head -1untuk RSA, atauopenssl pkey -in key.pem -nooutuntuk algoritma apa pun. Berhasil berarti byte-nya memang kunci yang sah dan masalahnya ada di sisi pustaka. Gagal berarti berkasnya rusak dan Anda lanjut ke langkah 4. - Periksa baris pustaka Anda di matriks. Bagian 4. Kalau Anda di Java dengan berkas PKCS#1, atau di Go dengan pemanggilan fungsi parse yang keliru, urusan Anda selesai di sini.
- Lihat karakter yang tak terlihat.
head -c 32 key.pem | xxdmenampilkan byte pertama, yang menangkap BOM, spasi di depan, dan pembatas terindentasi sekaligus dalam satu pandangan. Lalu pastikan baris-----BEGINdan-----ENDmulai di kolom nol, sesuai Bagian 5. - Belah masalahnya dengan kunci yang pasti benar. Buat pasangan baru di Generator Kunci RSA Online, arahkan kode Anda ke sana, lalu lihat apakah errornya bertahan. Kalau bertahan, bug-nya ada di kode pemuatan Anda, bukan di berkas kuncinya, dan memformat ulang berkas asli sebanyak apa pun tidak akan menolong. Kalau errornya hilang, berkas aslinyalah yang salah dan sekarang Anda punya kunci yang berfungsi sebagai bahan pembanding.
- Periksa algoritma dan ukurannya paling akhir. Pastikan header-nya bertuliskan
RS256, dan pastikan kuncinya minimal 2048 bit, sesuai Bagian 7.
Langkah 5 paling sering dilewati dan paling banyak menghemat waktu. Satu kunci pembanding yang bersih mengubah “kuncinya tidak jalan” yang samar menjadi jawaban biner tentang sisi mana yang rusak.
FAQ
Apa bedanya BEGIN RSA PRIVATE KEY dan BEGIN PRIVATE KEY?
Keduanya adalah dua kontainer di sekeliling kunci RSA yang sama. BEGIN RSA PRIVATE KEY adalah PKCS#1 dan memuat angka-angka RSA secara langsung; BEGIN PRIVATE KEY adalah PKCS#8 dan menambahkan algorithm identifier, yang membuatnya juga bisa membawa kunci ECDSA dan Ed25519. Mana yang Anda butuhkan sepenuhnya bergantung pada pustakanya, dan Generator Kunci RSA Online bisa menulis keduanya.
Kenapa openssl genrsa menghasilkan format yang berbeda dari tutorial?
Karena nilai default-nya bergeser. Di OpenSSL 3.6.2, openssl genrsa -out k.pem 2048 menulis PKCS#8 dengan header BEGIN PRIVATE KEY. Untuk mendapat susunan PKCS#1 tradisional seperti yang digambarkan panduan lama, tambahkan -traditional. Jalankan head -1 pada keluarannya alih-alih memercayai tutorial mana pun soal apa yang dihasilkan build Anda.
Bagaimana cara memperbaiki algid parse error, not a sequence di Java?
Pesan itu di Java 1.8.0_162 berarti Anda memberikan kunci PKCS#1 ke PKCS8EncodedKeySpec. Pustaka standarnya sama sekali tidak membaca PKCS#1. Konversikan sekali dengan openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem, atau tambahkan BouncyCastle kalau ada bagian lain di proyek yang memang sudah membutuhkannya.
Apakah setiap baris kunci privat harus mulai di kolom nol?
Tidak, dan nasihat umum soal ini justru terbalik. Diuji di Node v25.8.2, mengindentasi hanya baris isi base64 tetap bisa di-parse, sementara mengindentasi hanya baris -----BEGIN atau hanya baris -----END menyebabkan kegagalan. Satu baris kosong sebelum PEM diterima; satu spasi di depan tidak.
Bagaimana cara menyimpan kunci privat di berkas .env?
Bisa sebagai satu baris dalam tanda kutip dengan escape \n yang Anda kembalikan saat pemuatan dengan .replace(/\\n/g, '\n'), atau sebagai satu baris base64 yang Anda dekode saat startup. Cara kedua lebih aman karena tidak ada konvensi escaping yang bisa disalahpahami oleh loader konfigurasi.
Bisakah saya memakai kunci 1024 bit dengan RS256?
Node v25.8.2 mem-parse kunci RSA 1024 bit tanpa error, tetapi kode sumber jsonwebtoken menolak menandatanganinya: secretOrPrivateKey has a minimum key size of 2048 bits, kecuali allowInsecureKeySizes diaktifkan. Buat saja kunci 2048 bit. Pesan itu menyebut jumlah bit, dan begitulah cara Anda membedakannya dari masalah format.
Kenapa muncul error format kunci privat RS256 yang menyebut butuh kunci asimetris padahal saya sudah memberikan berkas kunci privat?
Di kode sumber jsonwebtoken, secretOrPrivateKey must be an asymmetric key when using ${header.alg} muncul ketika alg adalah RS, PS, atau ES sementara kuncinya bukan kunci privat. Biasanya nilainya adalah string secret bergaya HS256 yang tertinggal dari konfigurasi sebelumnya. String acak adalah pasangan HS256 dan Generator JWT Secret; RS256 butuh pasangan kunci, bukan secret.
Kesimpulan
Kelas bug ini mahal bukan karena sulit. Satu string error menutupi tujuh penyebab di Node, pesan Java menunjuk ke ASN.1 padahal jawaban sebenarnya adalah “kontainernya salah”, dan nasihat format yang paling sering diulang di topik ini justru terbalik. Anda tidak bisa membaca sampai ketemu jawabannya, jadi Anda mengeliminasi: baris header, parse dengan OpenSSL, matriks pustaka, karakter tak terlihat, kunci pembanding yang pasti benar.
Ada dua kebiasaan yang mencegah kejadian berulang. Catat kontainer mana yang dibutuhkan setiap layanan, tepat di sebelah kuncinya di secret store Anda, karena batasan itu hidup di pustakanya dan bukan di kuncinya. Dan simpan satu pasangan kunci yang pasti benar di lingkungan pengembangan Anda murni sebagai kontrol, supaya pertanyaan pertama tentang kegagalan kunci apa pun mendapat jawaban ya-atau-tidak dalam satu menit.
Untuk pertanyaan yang lebih luas tentang cara menerbitkan, merotasi, dan membatasi cakupan kunci-kunci ini setelah berhasil dimuat, lihat Praktik Terbaik Keamanan JWT: Serangan dan Pertahanan.