作者TonyQ (自立而後立人。低调一阵)
看板Ajax
标题[node] Sign with PKCS(.p12) key
时间Sat Nov 17 15:33:08 2012
虽然标题是写 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": "
[email protected]",
//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)
1F:推 dreamseer:快拜! 11/17 15:39
※ 编辑: TonyQ 来自: 1.34.116.11 (11/17 15:51)
2F:推 skyman1999:超强,,, 11/17 23:47
3F:推 liaosankai:快M起来 lol 11/24 12:26