[node] Sign with PKCS 12 (.p12) key

看板Ajax作者 (自立而後立人。低調一陣)時間11年前 (2012/11/17 15:33), 編輯推噓3(300)
留言3則, 3人參與, 最新討論串1/1
雖然標題是寫 sign with P12 Key , 不過其實內文是要寫如何 pass google OAuth with JWT 。 其中有一個 step 是 sign with P12 Key, 因為 Google OAuth 給的 Key 是 PKCS 12 的, 如何正確的用他進行簽章這件事情我走了不少冤枉路。 最近搞這東西搞了超過16小時,雖然說是自己做著玩得 project , 但被認證這麼基本的東西擋在門外,整個是很焦慮。XD --------------------------------- 這一切的起源都是因為我想用 NodeJS 連 Google Calendar API, 自己新增跟更新特定 Calendar 上的 Calendar Event 。 --------------------------------- --------------------------------- 我們走 Google OAuth 2.0 ,說到 OAuth 有跟 FB API 打交道過的, 應該都不會太陌生,你需要給他 scope 跟一些有的沒的相關資料, 然後你會取得一個 accessToken ,接著你就可以拿這 accessToken 去作壞事。 讀完 API,首先當然就是要先取得 accessToken 首先我先讀了 Google Developer Doucment , 其中有好幾種作法。(詳情看下面連結的左側選單) http://goo.gl/qUZEL 後來決定採用 Service Account,也就是說我自己 server 就是個 user , 自己跟 server 要 accessToken ,而不是讓第三方使用者, 自己導向他們網站驗證取得 accessToken 的作法。 (理由單純是因為想自己測試用,要驗證很煩; 另一方面是想這種作法他們是怎麼搞驗證的。 ) --------------------------------- @首先先到 Google API Console 申請一個 API Key 先 Create Proejct ,然後選 API Access , 然後選擇開 client、類別選 Service account。 https://code.google.com/apis/console/b/0/ 接著你就會拿到一個 Google 說他們不會保存的 Private key file, 是 PKCS 12 結尾的(副檔名 .p12 )。 另外你會拿到一個密碼(key password),要記住有這回事,後面會用到, 這個密碼叫做 "notasecret" ,應該是固定的。 這個檔案要好好保存,掉了的話就要重新申請一把了。 然後你會看到這些資訊, http://screencast.com/t/Q8rDbn5AR 包含 ClientID,Email Address ,Public key fingerprints, 其中我們會用到的主要是 Email Address。 萬一你把 P12 Key 弄丟了也可以來這右邊點選 Generate new key。 @JWT 前面的文件其中提到要組一個叫 JWT 的東西,什麼是 JWT 呢, 他全名叫 JSON Web Token ,主要就是用來作線上驗證用的。 目前採用他的,根據我這兩天 Google 看到的,除這個 google OAuth 以外, 還有 Apple 的 In App Purchase 跟 Google 的電子錢包等。 JWT 到底是東西,我一開始根本就沒理他,反正欄位文件都有了,我就照著填, 我就是玩遊戲不看說明書的那種人啦(咦), 不過有興趣的人還是可以詳閱文件,瞭解其為何可以作為安全性的驗證。 http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html @先試著照文件組組看 JWT 文件還是這篇 http://goo.gl/qUZEL A JWT is composed as follows: {Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature} 注意其中有一個點(.),另外這裡都是 Base64 編碼。 @JWT Header JWT Header 在 Google OAuth 上是固定的,寫死的, 這個文件上有給了,不用懷疑照著作就對了。 var header = JSON.stringify({ "alg": "RS256", "typ": "JWT" }); 我是偷懶用 JSON.stringify 直接轉字串,方便閱讀也方便修改。 接著注意到他要做 Base64 Encode,對字串要做 base64 轉換很簡單, 透過 Buffer 作就好。 new Buffer(header).toString("base64") @JWT Claim Set 一樣,照著文件寫,這裡開始有玄機了。 var claims = JSON.stringify({ "aud": "https://accounts.google.com/o/oauth2/token", //到期時間(單位是秒,所以getTime 是 ms 要除 1000), //加上 3600 是指一小時以後過期,這是 Google 允許的最大時間 "exp": parseInt(new Date().getTime()/1000,10) +3600, //申請時間 "iat": parseInt(new Date().getTime()/1000,10), //申請者,這裡請用 Api console 那邊看到的 email address "iss": "xxxxxxxxxxx@developer.gserviceaccount.com", //Scope ,看你想申請用什麼服務就寫什麼 //這個不太好找,請自行努力,這裡只附上 Calendar 的 scope。 //我是從 http://goo.gl/7pbtn 下面 try it, //右方有 Authr using OAuth 2.0 點下去看到的。 "scope":"https://www.googleapis.com/auth/calendar" }); 照理說 Claim Set 應該沒問題,只是填資料而已,要特別小心手誤, 很多人會把 iss 填成 client id ,要填的是前面取得的 email address。 一樣要做 Base64 Encoded。 new Buffer(claims).toString("base64") @Computing the Signature 這個才是大魔王,非常麻煩的東西,也是本文重點, 他要先把前面做出來的兩個元素,透過你取得的 p12 Key 進行簽章(sign)。 幾件事情要注意: 1.用來簽章的是已經 base64 encode 的 header 跟 claims, 別傻傻的用原本的 json 字串來簽。 2.header 跟 claims 中間有一個點(.),但結尾沒有點。 var content = encodedHeader+"."+encodedClaims; 這裡步驟比較複雜,我們一步一步來: A.安裝 crypto ,這樣你才能進行加解密相關操作。 B.你拿到的是 PKCS 12 (P12) 的 key , 很不幸的是 nodejs 似乎沒有直接處理 p12 的 client,至少我沒搜到。 所以你得先找台有 open ssl 的機器(linux base的通常都有), 先把 private key 的部份以 RSA 的形式繪出。 (或者傻瓜點的講法,把 P12 轉 Pem。) 指令是 openssl pkcs12 -in my-privatekey.p12 -out privatekey.pem -nodes -nodes 是指建立 privatekey 時,不需要再用 passphase進行加密。 把 my-privatekey.p12 改成你從 google 拿到的那個檔案的檔名, privatekey.pem 則是即將建立的 pem 檔案的檔名。 這時候他會問你 password ,請輸入 "notasecret", 順利的話,這時你會取得 privatekey.pem C.簽章 這件事情很單純,透過 crypto 就行了,範例碼附於後, 但要記得先把 prviatekey.pem 放到同資料夾下: var fs = require("fs"), crypto = require("crypto"); function sign(content) { var key = fs.readFileSync('privatekey.pem');//讀pk,直接當key用 var sign = crypto.createSign('RSA-SHA256');//指定演算法 sign.update(content); //你要簽的內容 var sig = sign.sign(key,'base64'); //進行簽章動作,並指定為 base64輸出 return (sig); } var signed = sign(content); //把之前組好的 header 跟 claims 拿來簽, //最後再把相關資料一起加起來就大功告成了。 var jwt = content +"."+signed; //到這一步就可以先驗證 JWT 是否能正確讀取 //https://developers.google.com/in-app-payments/docs/jwtdecoder?hl=zh-tw 最後再作 Post 給 'https://accounts.google.com/o/oauth2/token' 這裡我用的是 restler ,我想這應該對寫 node 的人, 不會有太大障礙才對,也可以試試自己用的方法。 var rest = require('restler'); rest.post('https://accounts.google.com/o/oauth2/token', { data:{ grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer", assertion:jwt } }).on("complete", function(data, response) { console.log(data); //順利的話會看到 access_token ,不然就是錯誤訊息 }); btw base64 Url Safe 的處理,作不作都無所謂, 我測過了都會過,post 通常不需要處理這個。 ------------------------------------------------- 過關之後就會覺得好像很簡單,順便寫一下我卡關卡在哪: 1.找怎麼 sign 這件事情 google 很久,才找到一個可以用的範例。XD 然後我其實是一直覺得應該會有人寫好 p12 client 的, 不是很想自己作轉換 pem 的動作, 加上我又懷疑 pem 轉過去後,是不是會有變化導致我沒簽過。 這件事情一直到後來我去找 Google API Java Client , 直接看他在 Java 世界怎麼作跟怎麼發,用他作為對照組交叉測試很多次之後, 才確定 P12 裡面的 PK 跟轉出來的 pem 是同樣的無誤。 找 pk12 client 這件事情花不少時間, 然後一直找到 node tls 的參考資料,但那完全就是不同事。Orz 2.我一直不確定 SHA256withRSA 是不是等同於 RSA-SHA256 ,這是因為基礎知識不足。 3.我打從一開始就犯一個要命的致命錯誤,因為我寫一個函式幫助我轉換 base64, function toBase64(obj) { return new Buffer(obj).toString("base64"); } 然後前面兩個 claim 跟 header 運作很正常, 測出來的資料也就對,我就太相信他了。 另一方面是我第一個找到的 sign 的 sample 是這樣的 var key = fs.readFileSync('privatekey.pem');//讀pk,直接當key用 var sign = crypto.createSign('RSA-SHA256');//指定演算法 sign.update(content); //你要簽的內容 var sig = sign.sign(key); return (sig); 我根本沒注意到說這裡可以改成出 base64, 另一方面因為我知道 buffer 也可以用來接 binary, 我想說好吧,那就寫 var sig = toBase64(sign.sign(key)); 從這裡開始就全錯了......Orz 不是不能這樣寫,只是如果要這樣作的話,要明確指定進入的型別, 像是 var sig = new Buffer(sign.sign(key),"binary").toString("base64"); 這樣就會對。 可是最要命的事情是他還是可以產出 base64 的結果, 我猜大概是拿 toString() 的結果直接去做的吧。 我錯過這個檢核點非常多次,一直懷疑是 JWT content 的問題, 最後我終於發現是 sign 的 issue , 我用 Java 世界可以正常通過的 code 直接簽 "hello world", 過來 nodejs 簽 "hello world" 發現不一致, 過來交叉測試大概花了三個小時,才發現這個低級錯誤。 學到的教訓就是 Buffer 進出的型別要講得很清楚, 不然就會出這種無聲的錯誤。Orz 網路上教學如何用 NodeJS 作 JWT 的資料真的很少, 希望這個資料可以對大家有幫助。囧rz -- P12 = PKCS 12 ,是 public key + private key + password 的 keystore; PEM = X.509 cert ,public key + private key 的 key pair 其中 key 分為 RSA key 跟 DSA key 等不同類型。 -- Life's a struggle but beautiful. -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 1.34.116.11 ※ 編輯: TonyQ 來自: 1.34.116.11 (11/17 15:35)

11/17 15:39, , 1F
快拜!
11/17 15:39, 1F
※ 編輯: TonyQ 來自: 1.34.116.11 (11/17 15:51)

11/17 23:47, , 2F
超強,,,
11/17 23:47, 2F

11/24 12:26, , 3F
快M起來 lol
11/24 12:26, 3F
文章代碼(AID): #1Gfpst3v (Ajax)