Modern HTML Email Development

今天在 Modern Web 分享的主題

其實最主要是想介紹 MJML 這個工具,不過最後介紹的篇幅有些不夠,有些可惜,話說今天設備也有些狀況,一是投影機解析度和預期的不一樣,二是無線麥克風聲音會延遲,對於講者來說還蠻干擾的,最後時間還剩的比預期多,覺得愧疚啊。

最後附上這次介紹的一些資源的連結,方便取用:


JSON Web Token

大丸百貨

之前的 JSON Universe 那篇文章在寫的時候,還沒發現到有這東西,直到上個月才發現到 JSON Web Token(JWT) 這個標準,研究過後覺得要單獨介紹一下,不過由於相關的標準有好幾個,花了些時間才搞清楚各個標準之間的關係;這一系列標準是由 JOSE(JSON Object Signing and Encryption) Working Group 所制訂的 RFC 標準,目前包括了:

共五個 RFC 標準,事實上,JSON Web Token 是要最後談到的;這一系列標準的目的是提供一個標準的協定,用在傳輸 JSON 資料時提供可靠性(簽章、signature)和安全性(加密、encryption),眼尖的人可能發現了,怎麼沒有 JOSE 的文件呢?事實上是真的沒有,而且也沒官方文件清楚解釋 JOSE 到底是什麼,最常看到的詞就是 JOSE Header 了,思考許久後才理解,JOSE 其實包括了兩種格式,分別是 JSON Web Signature(JWS) 和 JSON Web Encryption(JWE),JWS 只是加上驗證用的簽章,其實內容是明碼的,JWE 才是真的有把傳輸的資料加密過,至於簽章和加密用的演算法則是用 JWA 格式來紀錄,然後需要用到的 key,例如用非對稱加密保護加密內容的 key 給收信端,這時則是用 JWK 格式來記錄要使用的 public key,而這些資訊就是 JOSE Header 的內容了,JWK 和 JWA 都是很簡單的格式,基本上就是一個物件,然後有定義好的屬性:

{
  "kty":"EC",
  "crv":"P-256",
  "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
  "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
  "kid":"Public key used in JWS spec Appendix A.3 example"
}

例如這個 JWK 文件範例中的kty代表的是 Key Type;crvxy則是橢圓曲線加密(ECC)類演算法會用到的參數;kid則是自訂的 Key ID,用來在一堆 JWK 當中尋找所要的 key 使用;其它還有像是 X.509 憑證驗證會需要的資訊,各種加密演算法會用到的 Initialization Vector、Salt 等,都有定義好的屬性名稱。

JWS 和 JWK 就比較複雜些了,以 JWS 來說,你會先有要傳輸的資料 payload,然後一組 meta data,又稱為 JOSE Header,內容基本上就是 JWA + JWK + 一些基本的屬性,像是ctytyp

{
  "typ":"JWT",
  "alg":"HS256"
}

這就是一個最簡單的 JOSE Header,它說明傳輸的資料內容和簽名用的 HMAC 演算法,然後這個 JOSE Header 和 payload 要分別轉成 base64url 編碼,其實和 base64 沒差很多,就先把 JSON String 轉成 base64 encoding string 後,把 padding 的=都拿掉,然後+-取代,/_取代。例如ab?ab這個 ASCII 字串,用 base64 encoding 就會變成:

YWI/YmE=

用 base64url 的話就變成:

YWI_YmE

然後現在我們有一個 JOSE Header 和一個要傳輸的 JSON payload,以 base64url 編碼呈現並且用.接起來:

BASE64URL(UTF8(JOSE Header)) || '.' || 
BASE64URL(JWS Payload))

接著把這個字串拿去用 JOSE Header 裡面指定的 HMAC 演算法搭配一組 key 來算出簽章(signature),至此我們就有了 JWS 三樣必須的元素了:

  • JOSE Header
  • Payload
  • Signature

JWS 文件中定義了兩種格式可以用來傳輸這三個元素,第一種是精簡格式(Compact Serialization Syntax),格式很簡單,就和上面算 signature 用的格式一樣,只是現在多加了 signature 在後面,一樣用 base64url 形式:

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)

長的會看起來像是:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

另外一種則是 JSON 格式(JSON Serialization Syntax):

{
  "payload":"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9",
  "signatures":[
     {"protected":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
      "signature":"TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"}]
}

這種形式其實是最完整的版本,還可以加上 public header (沒 signature 驗證)和多個 signature;另外也有 flatten 版,只能放一個 signature:

{
  "payload":"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9",
  "protected":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
  "signature":"TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
}

雖然有 JSON 格式的 JWS,不過 精簡格式目前應該是最廣為通行的,一來是它資料量比較小,二是它比較方便在不同環境下傳輸使用,例如後面會提到的,放在 HTTP Header 內,如果沒有特殊需求要多個 signature,實在很沒有用 JSON 格式的需求。

最後,JWS 還有一個特殊的 case,就是它其實允許不加上簽章的,使用這組 JOSE Header:

{"alg":"none"}

然後 signature 是空字串,所以精簡格式的就會變成:

eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.

最後是.結尾。

JWE 和 JWS 的狀態其實也很像,只是三個元素變成五個,包括了:

  • JOSE Header
  • Encrypted Key
  • Initialization Vector
  • Ciphertext
  • Authentication Tag

其中 JOSE Header 和 JWS 的內容差不多,Encrypted Key 和 Initialization Vector(IV) 是加密時的輸入,這邊的 Encrypted Key 是一把加密過的 Key,被加密保護的 Key 又稱為 Content Encryption Key(CEK),是實際上用來加密保護內容時所使用的 Key,這把 CEK 和 IV 都是亂數產生的,那又有一個問題是,用什麼 Key 加密 CEK 來產生 Encrypted Key 呢?這邊建議的是用非對稱加密,拿收信方的 public key 來加密,當然 JOSE Header 裡面也可以塞進 x.509 的相關資訊用來確保 public key 的正確性;最後兩個,Ciphertext 和 Authentication Tag 則是加密的輸出,Authentication Tag 是 authentication encryption 會產生的,用來驗證內容正確性的資訊,就像是 JWS 的 signature 一樣用途,主要也是避免 Ciphertext 被用中間人攻擊替換掉,不過我還不太清楚如果可以偷到 key 偽造出 Ciphertext,是怎樣會沒法同時有另外一組 Authentication Tag 就是了。

然後一樣有精簡格式和 JSON 格式,精簡格式看起來就如下:

eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ

注意找的話就可以發現四個.把資料切成五段。

最後終於要來介紹 JSON Web Token(JWT)了,JWT 是什麼呢,它其實就是一個 JOSE 的應用,一句話來說,就是使用 JWS 或 JWE 來做現在網路服務的身分認證協定中常見的 token(權杖)的傳遞。所以 JWT 其實就是規範了一組 JSON 資料的屬性(claim),和身分認證相關,然後要求這個 JSON 資料要用 JWS 或 JWE 來傳輸提供保護,這些預先定義好的屬性有:

  • iss, Issuer
  • sub, Subject
  • aud, Audience
  • exp, Expiration Time
  • nbf, Not Before
  • iat, Issued At
  • jti, JWT ID

都是非常 meta 的 token 屬性,這些名稱基本上是從 OpenID Connect 那邊來的,除了這些定義好的屬性之外,還可以加上其它自訂的資料,只是這些已經被定義且註冊好的名稱不能另做他用,OpenID Connect 也有不少個人資料的屬性已經註冊上 IANA 了,像是first_namecountry之類的 profile 資訊,有一種使用 JWT 的方法就是直接把個人 profile 存在客戶端,server 只要驗證簽名是否正確,這樣一個好處是 server 不用保存 session 資訊,減少很多資源的需求,實做起來其實複雜度也比較低。另外由於是用作 token 之用,自然也可以當成 OAuth 的 token 使用,這部分資訊在 RFC-7523 這份文件有說明,至於要如何使用 OAuth token 則是在 RFC-6750 有介紹,比較常見的是放在 HTTP Auth Header 裡面:

Authorization: Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.

之前鴨七也有整理過中文的說明,看起來比較輕鬆,而且說明很完整(不過我承認我沒有從頭看到尾)。

目前搜尋 JWT 一定會看到一個網站 jwt.io,這個網站用淺顯易懂的方式來介紹 JWT,把比較複雜的關係,像是 JWS、JWE 等等都隱藏起來幫助瞭解,還有一個線上除錯工具和不同語言的 library 整理,不過除錯工具只有支援 JWS,另外也有和一些其它類似標準做比較,還蠻值得看一看的,這個網站是由 Auth0 提供的,他們其實就是一家專門提供身分認證服務的公司,似乎都已經轉到使用 JWT 了,在 jwt.io 有提到他們似乎是把他用作 stateless 的 token 來用,我對這間公司之前印象是還不錯,一來是 API 文件看起來還蠻不錯的,另外有不少的開放原碼專案,當然有一些是串接他們家服務用的 library 啦。

PS. 對密碼學還沒很熟悉,有誤歡迎指正~


更之前的文章