LoginSignup
4
4

More than 3 years have passed since last update.

CryptoKit を使って ECDSA を用いた署名を実装する

Posted at

ECDSAでJWTによる電子署名を作成することがあったのでメモ :pencil:

ECDSAとは

楕円曲線暗号(だえんきょくせんあんごう、Elliptic Curve Cryptography、ECC)とは、 楕円曲線 上の 離散対数問題 (EC-DLP) の困難性を安全性の根拠とする 暗号

  • 暗号化と復号とで異なる2つの鍵を使用し、暗号化の鍵を公開できるようにした公開鍵暗号
  • RSA暗号 と比べて、短いデータ長で処理速度も早いが同レベル安全性が実現できるのがメリット

CryptoKit を使ってやること

CryptoKitを使って以下の手順により署名を作成していきます。

  1. 鍵の生成
  2. 電子署名の作成

全体のコードはこちらに上げてあります。

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.ECDSASignaturederRepresentationrawRepresentation の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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4