Saya memiliki aplikasi NodeJS yang menghasilkan Token Web JSON dengan algoritma PS256. Saya ingin mencoba dan memverifikasi tanda tangan di token ini dalam aplikasi PHP.

Sejauh ini saya punya yang berikut:

JWT saya:

eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTBiYjllYS00YTg0LTQ1ZTMtOTg5My0wYzNhNDYxZmQzMGUiLCJpYXQiOjE2MDU4OTI5NjcsImV4cCI6MTYwNjQ5Nzc2NywiYXVkIjoiNzBiYzcxMTQ1MWM2NDBjOTVlZjgzYjdhNDliMWE0MWIiLCJpc3MiOiIyM2ZhYTRiNC0wNmVlLTRlNGEtYTVjZC05NjJmOTRhMjEzYmYiLCJqdGkiOiI1MTNiYjczZC0zOTY3LTQxYzUtODMwOS00Yjc1ZDI4ZGU3NTIifQ.kLtaSYKyhqzx7Dc7UIz7tqU8TsXabRLxGiaqw21lgCcuf_eBvpiLkFOuXpUs-V8XQunQg8jV-bKlKUIb0pLvipjhRP50IwKDClQgNtIwn4yyX5RyDNGJur0qHNnkHMLaF11NsXGPyhvh-6ogSZjWgyZnkQJkXpz4jggBetwqz1hnicapGfNb6C-UdRcOLyCaiMD4OmvniFVCY6YoKlC6eHdwxsgHAxOSgD1QKiiQX_yAe39ja_axZD2Ii3QaNgO0WXzfWMbqRg_yl0y3kjQFys9iXGvQ1JIKDMLffR3rKVL5PgKSU3e472xcPKf6PNSJzphPi1G_xH2gqg1VVXo3Lg

Didekode:

Header:
(
    [alg] => PS256
    [typ] => JWT
)

Body:
(
    [sub] => 010bb9ea-4a84-45e3-9893-0c3a461fd30e
    [iat] => 1605892967
    [exp] => 1606497767
    [aud] => 70bc711451c640c95ef83b7a49b1a41b
    [iss] => 23faa4b4-06ee-4e4a-a5cd-962f94a213bf
    [jti] => 513bb73d-3967-41c5-8309-4b75d28de752
)
  • sub adalah ID pengguna GUID (kami menggunakan GUID sehingga jika ID pengguna bocor, tidak ada informasi yang dapat diekstrapolasi, seperti jumlah pengguna di sistem kami atau saat pengguna mendaftar)
  • iat adalah waktu saat token dikeluarkan (UTC)
  • exp adalah waktu epoch token akan kedaluwarsa (UTC)
  • aud tidak sesuai dengan spesifikasi JWT. Saya menyalahgunakan klaim ini untuk mengurangi efek token yang dicuri. Ini adalah hash MD5 data yang dikirim dengan setiap permintaan klien yang akan sulit ditebak oleh seseorang. Jadi jika seseorang mencuri token ini dan menggunakannya tanpa mengirimkan kata sandi yang sesuai, token tersebut akan otomatis dicabut.
  • iss juga tidak sesuai dengan spesifikasi JWT. Saya menyalahgunakan klaim ini untuk mencantumkan ID kunci yang digunakan untuk menandatangani JWT. Dengan cara ini saya dapat merotasi pasangan kunci publik-swasta saya dan mengetahui kunci mana yang digunakan saat memvalidasi tanda tangan
  • jti adalah GUID yang secara unik mengidentifikasi JWT. Dibandingkan dengan penyimpanan token yang dicabut dalam memori

Saya menggunakan algoritma PS256 melalui RS256 karena saya membaca di posting blog bahwa itu lebih aman. Sejujurnya saya tidak tahu bedanya.

Saya menggunakan algoritma PS256 daripada ES256 karena setelah pengujian saya menemukan bahwa sementara ES256 menghasilkan tanda tangan yang lebih kecil (dan karena itu token yang lebih kecil), butuh sekitar 3x lebih lama untuk menghitung. Tujuan saya adalah membuat aplikasi ini scalable mungkin, sehingga waktu komputasi yang lama harus dihindari.

Kunci publik saya:

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA0wO7By66n38BCOOPqxnj78gj8jMKACO5APe+M9wbwemOoipeC9DR
CGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2klyKF7dGQwecgf3So7bve8vvb+vD8C6l
oqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawym+OcE/0pzWlNV9asowIFWj/IXgDVKCFQ
nj164UFlxW1ITqLOQK1WlxqHAIoh20RzpeJTlX9PYx3DDja1Pw7TPomHChMeRNsw
Z7zJiavYrBCTvYE+tm7JrPfbIfc1a9fCY3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCm
QhPE4jcDINUds8bHu2on5NU5VmwHjQ46xwIDAQAB
-----END RSA PUBLIC KEY-----

Menggunakan jsonwebtoken untuk NodeJS, saya dapat memverifikasi token ini dan mengotorisasi permintaan yang dibuat dengan menggunakannya. Jadi semua data tampak bagus, kuncinya berfungsi, dan matematikanya berhasil.

Namun saya mengalami dua masalah saat mencoba memverifikasi token di PHP:

1. Kunci publik sepertinya tidak valid?

$key = openssl_pkey_get_public($pem);
print_r($key);
die();

Kode ini mencetak "salah" - menunjukkan bahwa kunci tidak dapat dibaca dari teks PEM yang diposting di atas. Googling sekitar saya menemukan komentar ini di manual PHP yang memberikan solusi. Saya melakukan seperti yang diinstruksikan (menghapus baris baru dari kunci saya, menambahkan MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A, lalu dibungkus menjadi 64 karakter) dan untuk beberapa alasan openssl_pkey_get_public($pem) sebenarnya mengembalikan Kunci Publik OpenSSL sekarang. Saya tidak terlalu tertarik menggunakan solusi salin/tempel yang saya tidak mengerti, dan komentar menyebutkan bahwa ini hanya akan berfungsi untuk kunci 2048-bit, yang menjadi perhatian saya jika kami ingin meningkatkan keamanan kami di masa mendatang.

Setelah melakukan perubahan yang disarankan ke kunci saya, kunci baru terlihat seperti ini:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wO7By66n38BCOOPqxnj
78gj8jMKACO5APe+M9wbwemOoipeC9DRCGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2
klyKF7dGQwecgf3So7bve8vvb+vD8C6loqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawy
m+OcE/0pzWlNV9asowIFWj/IXgDVKCFQnj164UFlxW1ITqLOQK1WlxqHAIoh20Rz
peJTlX9PYx3DDja1Pw7TPomHChMeRNswZ7zJiavYrBCTvYE+tm7JrPfbIfc1a9fC
Y3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCmQhPE4jcDINUds8bHu2on5NU5VmwHjQ46
xwIDAQAB
-----END PUBLIC KEY-----

(perhatikan bahwa ini adalah kunci yang sama, hanya dengan 32 byte ajaib yang ditambahkan di awal dan "BEGIN RSA PUBLIC KEY" diganti dengan "BEGIN PUBLIC KEY")

2. Tanda tangan gagal diverifikasi (mungkin karena saya menggunakan PS256 dan bukan RS256)

Mengabaikan masalah dengan #1 untuk saat ini dan melanjutkan ke langkah berikutnya, saya mencoba memverifikasi tanda tangan saya seperti:

$success = openssl_verify($jwtHeader . "." . $jwtBody, $jwtSignature, $key, OPENSSL_ALGO_SHA256);

Ini mengembalikan false. Artinya tanda tangan itu tidak sah. Tapi saya tahu tanda tangannya valid karena berfungsi dengan baik di NodeJS. Jadi saya menduga masalah di sini berkisar pada pilihan algoritme saya.

Bagaimana cara agar PHP memverifikasi token ini dengan benar?

Perbarui 1

Berikut kode yang saya gunakan untuk memverifikasi token saya di NodeJS. Ini adalah proyek HapiJS + TypeScript, tetapi Anda harus dapat memahaminya. jwt baru saja didefinisikan sebagai import * as jwt from "jsonwebtoken";:

                        jwt.verify(
                            token,
                            server.plugins["bf-jwtAuth"].publicKeys[tokenData.iss].key,
                            {
                                algorithms: [options.algorithm],
                                audience: userHash,
                                maxAge: options.tokenMaxAge
                            },
                            err =>
                            {
                                // We can disregard the "decoded" parameter
                                // because we already decoded it earlier
                                // We're just interested in the error
                                // (implying a bad signature)
                                if (err !== null)
                                {
                                    request.log([], err);
                                    return reject(Boom.unauthorized());
                                }

                                return resolve(h.authenticated({
                                    credentials: {
                                        user: {
                                            id: tokenData.sub
                                        }
                                    }
                                }));
                            }
                        );

Tidak banyak yang bisa dilihat di sini, karena saya hanya mengandalkan alat pihak ketiga untuk melakukan semua validasi untuk saya. jwt.verify(...) dan itu bekerja seperti sulap.

Perbarui 2

Dengan asumsi bahwa masalah saya terletak pada algoritma yang digunakan (PS256 vs RS256) saya mulai mencari-cari dan menemukan postingan StackOverflow ini yang mengarahkan saya ke rel phpseclib

Kami sebenarnya secara kebetulan sudah menginstal phpseclib melalui Composer sebagai ketergantungan SDK auth Google, jadi saya meningkatkannya ke ketergantungan tingkat atas dan mencobanya. Sayangnya saya masih mengalami masalah. Inilah kode saya sejauh ini:

use phpseclib\Crypt\RSA;

// Setup:
$rsa = new RSA();
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);

// The variables I'm working with:
$jwt = explode(".", "..."); // [Header, Body, Signature]
$key = "..."; // This is my PEM-encoded string, from above

// Attempting to verify:
$rsa->loadKey($key);
$valid = $rsa->verify($jwt[0] . "." . $jwt[1], base64_decode($jwt[2]));
if ($valid) { die("Valid"); } else { die("Invalid"); }

Pernyataan die() tidak tercapai saat saya menemukan kesalahan pada baris $rsa->verify() dengan yang berikut:

ErrorException: Invalid signature
at
/app/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php(2693)

Melihat baris ini di perpustakaan, sepertinya gagal pada langkah "pemeriksaan panjang":

if (strlen($s) != $this->k) {
    user_error("Invalid signature");
}

Saya tidak yakin berapa panjang yang diharapkan. Saya melewati tanda tangan mentah langsung dari JWT

1
stevendesu 20 November 2020, 20:05

1 menjawab

Jawaban Terbaik

Setelah bermain-main dengan ini sepanjang hari, saya akhirnya menemukan bagian yang hilang.

Pertama, beberapa catatan singkat tentang pertanyaan awal (sudah disinggung dalam pembaruan):

  • Untuk melakukan tanda tangan RSA dengan padding PSS ("PS256") Anda memerlukan perpustakaan pihak ketiga, karena fungsi OpenSSL di PHP tidak mendukung ini. Rekomendasi umum adalah phpseclib
  • 32 byte ajaib yang harus saya tambahkan ke kunci hanyalah kekhasan fungsi OpenSSL PHP dan tidak perlu digunakan dengan phpseclib

Dengan itu, masalah terakhir saya (dengan tanda tangan "tidak valid") adalah:

Tanda tangan JWT dikodekan base64URL, bukan dikodekan base64

Saya bahkan tidak menyadari ada spesifikasi RFC untuk pengkodean base64URL. Ternyata Anda hanya mengganti setiap + dengan - dan setiap / dengan _. Jadi alih-alih:

$signature = base64_decode($jwt[2]);

Harus:

$signature = base64_decode(strtr($jwt[2], "-_", "+/"));

Ini berfungsi dan tanda tangan saya akhirnya divalidasi!

0
stevendesu 20 November 2020, 21:03