Convert ECC x y point to PEM Python

剛好看到網友發文,要解Yahoo的簽章,但公鑰的格式竟然是給ECC公鑰的x, y兩個點,所以稍微研究了一下。

發生了什麼

網友在IT幫幫忙提了一個問題:yahoo 第三方授權 jwt 驗證簽名這一段怎麼做?? – iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天 (ithome.com.tw)

我們知道JSON Web Tokens – jwt.io可以幫忙驗證各種形式的JWT,所以首先先把網友貼出來的JWT先丟上去看看。

eyJhbGciOiJFUzI1NiIsImtpZCI6IjM0NjZkNTFmN2RkMGM3ODA1NjU2ODhjMTgzOTIxODE2YzQ1ODg5YWQifQ.eyJhdF9oYXNoIjoiTlN3cDVTTmZWb2NRVllaNkgyb2NrQSIsInN1YiI6Ik01WlFJS0dONVlSTVBWR1FHRTVFRkVXSFBFIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImJpcnRoZGF0ZSI6IjE5ODEiLCJnZW5kZXIiOiJvdGhlciIsImlzcyI6Imh0dHBzOi8vYXBpLmxvZ2luLnlhaG9vLmNvbSIsInByb2ZpbGVfaW1hZ2VzIjp7ImltYWdlNjQiOiJodHRwczovL3MueWltZy5jb20vYWcvaW1hZ2VzL2RlZmF1bHRfdXNlcl9wcm9maWxlX3BpY182NHNxLmpwZyIsImltYWdlMTkyIjoiaHR0cHM6Ly9zLnlpbWcuY29tL2FnL2ltYWdlcy9kZWZhdWx0X3VzZXJfcHJvZmlsZV9waWNfMTkyc3EuanBnIiwiaW1hZ2UxMjgiOiJodHRwczovL3MueWltZy5jb20vYWcvaW1hZ2VzL2RlZmF1bHRfdXNlcl9wcm9maWxlX3BpY18xMjhzcS5qcGciLCJpbWFnZTMyIjoiaHR0cHM6Ly9zLnlpbWcuY29tL2FnL2ltYWdlcy9kZWZhdWx0X3VzZXJfcHJvZmlsZV9waWNfMzJzcS5qcGcifSwiZ2l2ZW5fbmFtZSI6IuWLneWPsCIsIm1pZGRsZV9uYW1lIjoiIiwibG9jYWxlIjoiemgtSGFudC1UVyIsIm5vbmNlIjoiWWloc0Z3R0tndDNLSlVoNnRQczIiLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLnlpbWcuY29tL2FnL2ltYWdlcy9kZWZhdWx0X3VzZXJfcHJvZmlsZV9waWNfMTkyc3EuanBnIiwic2lkIjoiNFJaYlVrOWZOZlNHIiwiYXVkIjoiZGoweUptazlWMHhOUWtST1ZHTmxlWGsxSm1ROVdWZHJPVlJIWkRKaWVtUlhUa1JKYldOSGJ6bE5RVDA5Sm5NOVkyOXVjM1Z0WlhKelpXTnlaWFFtYzNZOU1DWjRQV1UzIiwiYXV0aF90aW1lIjoxNjQ5OTI2MzA3LCJuaWNrbmFtZSI6IuWLneWPsCIsIm5hbWUiOiLmiLTli53lj7AiLCJzZXNzaW9uX2V4cCI6MTY1MTEzNTkwNywiZXhwIjoxNjUwNDM2Nzg0LCJpYXQiOjE2NTA0MzMxODQsImFwcF9pZCI6ImRqMHlKbWs5VjB4TlFrUk9WR05sZVhrMUptUTlXVmRyT1ZSSFpESmllbVJYVGtSSmJXTkhiemxOUVQwOUpuTTlZMjl1YzNWdFpYSnpaV055WlhRbWMzWTlNQ1o0UFdVMyIsImZhbWlseV9uYW1lIjoi5oi0IiwiZW1haWwiOiJqYnVkdW9vMTIzQHlhaG9vLmNvbSJ9.LhmytVBJ-BWrmeZMbDWJh-sAFoOjdraRDkpxl0yA44eLSGLMbyliFMTlEgcPISWhif-Hg98dkySuyzzikhSaSw

看起來很順利的從裡面拿到了簽章方法

{
  "alg": "ES256",
  "kid": "3466d51f7dd0c780565688c183921816c45889ad"
}

是ES256呢,根據文件與網友的說明,Yahoo使用一個網頁來發布JWT的公鑰。

{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "01640f903a24e1c1f097ebda2d09b196c9de3ed9",
      "n": "APXkXeLRieppySH9dilwRSJjDW1MYOjSfqX8ifdXClRJiy6jsTlYcd2zDL6FPDeH6s2n_paJpnyEzzUTiSK-paC1yxdSAYPTT42tZqj5K5ZqBZgapYbPmbjEgCN80kPE66rGtRBkTehCAfAZ_CWFWX6yPDjpAMRkRWCDROdMoYm3",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "6ff94cdad11e7c3ac08dc9ec3c44844b87e364f7",
      "n": "AL1LkSgnGk-sKqFDBrojoqvpqOwmN7tgvz0p6J9g8O_nOzXMAwzMUUs4H_FMgeNWcuE6XzJX3spVwAYBp-rBLwyXXCGbO_chhwcpBDNndlZyqS2zOvwmZYdh4MhrUnIOcA8cdDB1hqoDdKOx9M-EjuoafcgqEPA7rWsZTH6TITMP",
      "e": "AQAB"
    },
    {
      "kty": "EC",
      "alg": "ES256",
      "use": "sig",
      "crv": "P-256",
      "kid": "3466d51f7dd0c780565688c183921816c45889ad",
      "x": "cWZxqH95zGdr8P4XvPd_jgoP5XROlipzYxfC_vWC61I",
      "y": "rxX9OCD9rIaheKx6LAs4KWR6Rz1-Lj1phRCmdjUDL_I"
    }
  ]
}

觀察可知,有兩種形式的三把公鑰,分別是RSA和ECC的形式。
RSA的公鑰可以由n和e推導出來,ECC的公鑰也可以由x和y推導出來,這是一個二維平面上的座標,可以標誌出特定的橢圓曲線。

但…無論是xy或是ne都應該是整數才對阿,這是什麼編碼呢?

這是網友提供的文件,上面有範例程式碼,這下好解決了,看來該串亂碼應該是URL_Safe的Base64編碼成二進位的樣子,URL_Safe就是將Base64中的+和/分別替換為- _兩個在網址中沒有特殊意義的符號。

解題

那麼該做的是情應該很簡單了,我這邊使用Python呈現。

  1. 先把兩個Base 64換回二進制編碼(補=使長度變成4的倍數)
    x_bin = base64.urlsafe_b64decode("cWZxqH95zGdr8P4XvPd_jgoP5XROlipzYxfC_vWC61I=")
    y_bin = base64.urlsafe_b64decode("rxX9OCD9rIaheKx6LAs4KWR6Rz1-Lj1phRCmdjUDL_I=")
  2. 二進制編碼轉成整數
    int.from_bytes(x_bin , "big")
  3. 用Python匯入
    b = ECC.construct(curve="p256", point_x=int.from_bytes(x_bin , "big"), point_y=int.from_bytes(y_bin , "big"))
  4. 將證書轉成PEM格式輸出 print(b.export_key(format='PEM'))

經過這些步驟,就可以輸入Jwt.io驗證簽章了

ECC驗證成功

完整程式碼

from Crypto.PublicKey import ECC
import base64

b = ECC.construct(curve="p256", point_x=int.from_bytes(base64.urlsafe_b64decode("cWZxqH95zGdr8P4XvPd_jgoP5XROlipzYxfC_vWC61I="), "big"), point_y=int.from_bytes(base64.urlsafe_b64decode("rxX9OCD9rIaheKx6LAs4KWR6Rz1-Lj1phRCmdjUDL_I="), "big"))
print(b.export_key(format='PEM'))

轉出來的公鑰

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcWZxqH95zGdr8P4XvPd/jgoP5XRO
lipzYxfC/vWC61KvFf04IP2shqF4rHosCzgpZHpHPX4uPWmFEKZ2NQMv8g==
-----END PUBLIC KEY-----

一些好玩的網站

Base64 to binary: Encode and decode bytes online — Cryptii

在 Python 中轉換二進位制為 Int | D棧 – Delft Stack

base64 —— Base16、Base32、Base64、Base85 資料編碼 — Python 3.10.4 說明文件

ECC — PyCryptodome 3.14.1 documentation

JSON Web Tokens – jwt.io

發佈留言