ECDSAでJWTによる電子署名を作成することがあったのでメモ
ECDSAとは
楕円曲線暗号(だえんきょくせんあんごう、Elliptic Curve Cryptography、ECC)とは、 楕円曲線 上の 離散対数問題 (EC-DLP) の困難性を安全性の根拠とする 暗号。
- 暗号化と復号とで異なる2つの鍵を使用し、暗号化の鍵を公開できるようにした公開鍵暗号
- RSA暗号 と比べて、短いデータ長で処理速度も早いが同レベル安全性が実現できるのがメリット
CryptoKit を使ってやること
CryptoKitを使って以下の手順により署名を作成していきます。
- 鍵の生成
- 電子署名の作成
全体のコードはこちらに上げてあります。
Key Pairの生成
CryptoKitの P256.Signing.PrivateKey を使えば2行で書けます
let privateKey = P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey
P256.Signing
はECDSA(P-256)を使用した署名、検証のためのもの
P256.KeyAgreement という鍵交換に使うためのものも用意されている
署名の作成
JWT Header
Headerに指定できる各パラメータについての説明は Registered Header Parameter Names を参照
{ "alg": "ES256", "typ": "JWT" }
JWT Claims
Claimsに指定できる各パラメータについての説明は Registered Claim Names を参照
{
"aud": "my-project",
"iat": 1509650801,
"exp": 1509654401
}
JWT signature
ECDSA P-256 の署名の例が JSON Web Signature (JWS) に書いてあります
Base64urlエンコードした JWT Header と JWT claims を .
でつなげたものを署名し、以下の順序で .
でつないだものがJWTとなります
{base64url-encoded header}.{base64url-encoded claim set}.{base64url-encoded signature}
署名にはP256.Signing.PrivateKey が持っている signature(for:) を使います
let input = "{base64url-encoded header}.{base64url-encoded claim set}"
let data = input.data(using: .utf8)!
let signature = try privateKey.signature(for: data)
ここで注意が必要なのが署名のフォーマットです。
ECDSA P-256 SHA-256の署名は、符号なし整数のECポイント(各32オクテットのR, S)で表されます。
signature(for:) が返してくる P256.Signing.ECDSASignature は derRepresentation と rawRepresentation の2つをもっていて、それぞれ以下の値が返ってきます。
-
derRepresentation
ASN.1 DER でフォーマットされた署名 -
rawRepresentation
署名のrawデータ
ECDSA P-256 SHA-256の署名では rawRepresentation
の方を使用します。
先述の通りJWTはbase64urlエンコードした header と claim set と signature をドットでつなぐので
extension Data {
func base64urlEncodedString() -> String {
return base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
let raw = signature.rawRepresentation
let signedJWT = "\(input).\(raw.base64urlEncodedString())"
これでJWTの完成です。
ちなみに、ASN.1 DERフォーマットは以下のような構造になっており、この中からrとsを取り出しつなげたものが rawRepresentation
に相当します。
0x30|b1|0x02|b2|r|0x02|b3|s
- b1: 0x02以降に続くバイト列の長さ
- b2: rのバイト列の長さ
- b3: sのバイト列の長さ
をそれぞれ表す
この仕様に基づいて以下のように derRepresentation
から raw data に変換してみると rawRepresentation
と同じ値が得られます。
let der = signature.derRepresentation
let sequence = der.removeFirst() // 0x30
let b1 = der.removeFirst()
let tag1 = der.removeFirst() // 0x02
let b2 = der.removeFirst()
var r = der.prefix(Int(b2))
der = der.advanced(by: Int(b2))
let tag2 = der.removeFirst() // 0x02
let b3 = der.removeFirst()
var s = der.prefix(Int(b3))
let octetLength = 32
guard r.count <= octetLength + 1, s.count <= octetLength + 1 else {
throw SignatureError.invalidLength
}
r = (r.count == octetLength + 1) ? r.dropFirst() : r
s = (s.count == octetLength + 1) ? s.dropFirst() : s
return r+s
Security Frameworkの SecKeyCreateSignature を使う場合、こちらは ASN.1 DERフォーマット で返されるので上記のような変換が必要です。
CryptoKitを使えば signature.rawRepresentation
だけで取得できるのですごく便利です。
まとめ
CryptoKitを使うと鍵の生成や署名が1行くらいでできてしまうのでとても簡単に使えて驚きました。
ただし、CryptoKitで生成した鍵は自分で保存する必要があるのでそれを考えると生成はSecurity Frameworkの SecKeyGeneratePair をつかってもいい気がしました。
CryptoKitで生成した鍵をキーチェーンに保存する方法はAppleがSampleコードを提供しているのでこれを参考に。
Storing CryptoKit Keys in the Keychain