公開鍵暗号をプログラムで扱う方法のまとめシリーズの終盤戦。ようやく実際のプログラム上で公開鍵暗号を使う話に入ります。
対象範囲
暗号化にもいろいろありますが、このシリーズでは、RSAに公開鍵暗号、電子署名だけをターゲットにしています。とはいえ、他の暗号化方式を使いたい方にも有用な情報はあると思います。
基本知識
非対称鍵による暗号や署名についての基本的な概念の理解を持っている必要があります。最低限以下の知識は必要です。
- 公開鍵とは何か
- 秘密鍵とは何か
- 証明書とは何か
- 公開鍵暗号は共通鍵暗号と比べてどういう特徴があるか
- 電子署名とは何か
- ダイジェスト値、ハッシュ値、ダイジェスト関数、ハッシュ関数とは何か
自信のない方は公開鍵暗号をプログラムで扱う方法のまとめシリーズを最初から読んでみてください。
iOSの暗号化フレームワーク
iOSでは Security Frameworkというものが用意されていて、これを使うと暗号や電子署名を利用することができます。
このSecurity Framworkではどんなことができるのかまとめてみました。
- (★)RSAの公開鍵を使って暗号化する
- (★)RSAの秘密鍵を使って平文化する
- (★)RSAの署名を生成する
- (★)RSAの署名を検証する
- RSAの鍵ペアを生成する
- 生成された鍵ペアを Keychainに保存すること
- Keychainに保存された鍵ペアを読み出すこと
- (★)OpenSSL コマンドで生成した公開鍵を読み込む
- (★)OpenSSL コマンドで生成した個人情報交換ファイル(.p12)を読み込む
- 個人情報交換ファイルには秘密鍵を含められるので、秘密鍵の読み込みもできます
本エントリでは、★のついた機能を使う方法をご紹介します。
OpenSSL について
Appleが暗号化のために用意しているSecurity Frameworkの他に、暗号化ライブラリの老舗である OpenSSLというものがあります。
OpenSSLはプログラムに組み込むライブラリとして使用するものですが、コマンドラインツールとしても使えます。opensslコマンドは多くのUNIXシステム(OS X含む)にデフォルトでインストールされていて、鍵の生成や暗号化、署名の検証など様々な用途に使うことができます。また、OpenSSLライブラリを iOS向けにビルドしてリンクすることで、Security FrameworkではできないことをOpenSSLライブラリを使っておこなうこともできます。
このように、iOSで暗号を扱う上で OpenSSLの理解は必要不可欠と言えます。
とくに理解しておきたいのは以下のポイントになります。
- 暗号に使用する鍵や証明書の生成方法
- 暗号化や平文化の方法
- 署名の生成や検証方法
- 鍵ファイルや証明書ファイルのデータ構造について
- データ構造と拡張子との対応
- 鍵ファイルの構造をダンプする方法
この辺りの詳細も、以下のエントリにまとめてあるので、不安な方は一度目を通してください。
Swiftから Security Frameworkを使う具体的な方法
それでは本題の Security Frameworkの使い方に入ります。Security Frameworkを使ったサンプルコードとしては、Appleの公式サイトにある以下のコードやドキュメントが大変役に立ちます。
この情報をベースに、もう少し噛み砕き、Swiftベースで書き直したものを以下に示します。
事前準備
Swiftから Security Frameworkを使う場合は、ソースコードの頭に、
import Security
を入れてください。また、電子署名などでハッシュ関数を利用する際は、CommonCrypto.h という Cのヘッダファイルをインクルードする必要があります。そのような場合、Swiftのコードからは直接インポートできないので、Bridging Headerファイルを用意して、以下の文を追加してください。
#import <CommonCrypto/CommonCrypto.h>
Bridging Headerファイルは、Xcodeに作らせるのが簡単です。SwiftベースのプロジェクトをXcode上で開いた状態で Objective-Cのソースを新規に作成すると、「Bridging Header作りますか?」と聞いてくれるので、OKを押せばOKです。そのあと、不要なObjective-Cのファイルは消してOKです。
OpenSSLで生成した公開鍵を読み込む
Security Frameworkでは、OpenSSLで生成した公開鍵のうち、DERエンコードのものを読み込むことができます。もし、手元の鍵がPEMエンコードの場合はこちらのエントリを参照してDERエンコードに変更しておいてください。
Framework上では公開鍵はSecKey
というデータ型で表現されますが、ファイルから直接SecKey
を生成することはできず、以下の段階を踏む必要があります
- 公開鍵ファイル(DERエンコード)を読み込んで
SecCertificate
(証明書を取り扱うデータ型)を生成 -
SecCertificate
からSecTrust
を生成 -
SecTrust
からSecKey
(公開鍵)を取り出す
以下に具体的なコード例を示します。
/**
公開鍵をDERエンコードされたファイルから読み込みます。
:param: publicKeyPath 公開鍵ファイルのパス
:returns: 読み込まれた公開鍵。読み込みに失敗した場合はnil
*/
private func loadPublicKey(publicKeyPath: String) -> SecKey? {
// 鍵ファイルをNSDataとして読み込む
let publicKeyFileContent = NSData.dataWithContentsOfMappedFile(publicKeyPath) as NSData?
if (publicKeyFileContent == nil) {
NSLog("Can not read public key from \(publicKeyPath)");
return nil
}
// 鍵ファイルの内容を読み込み、SecCertificateを生成する
let c = SecCertificateCreateWithData(kCFAllocatorDefault, publicKeyFileContent!)
if (c == nil) {
NSLog("Can not read certificate from \(publicKeyPath)")
return nil
}
let certificate = c.takeRetainedValue()
// SecCertificateからSecTrustを生成
let policy = SecPolicyCreateBasicX509().takeRetainedValue()
var t: Unmanaged<SecTrust>? = nil
let returnCodeCreateCertificates = withUnsafeMutablePointer(&t) {
tPtr in
SecTrustCreateWithCertificates(certificate, policy, tPtr)
}
if (returnCodeCreateCertificates != 0) {
NSLog("SecTrustCreateWithCertificates fail. Error Code: %ld", returnCodeCreateCertificates)
return nil;
}
let trust = t!.takeRetainedValue()
// SecTrustを検証する
// 証明書ではなく、信頼できる公開鍵を直接読み込む場合などは検証しなくてもよい
var trustResultType = SecTrustResultType()
let returnCodeEvaluate = withUnsafeMutablePointer(&trustResultType) {
(trustResultTypePtr:UnsafeMutablePointer<SecTrustResultType>) in
SecTrustEvaluate(trust, trustResultTypePtr)
}
if (returnCodeEvaluate != 0) {
NSLog("SecTrustEvaluate fail. Error Code: %ld", returnCodeEvaluate)
return nil;
}
// SecTrustから公開鍵(SecKey)を取り出す
let publicKey = SecTrustCopyPublicKey(trust).takeRetainedValue() as SecKey?
if (publicKey == nil) {
NSLog("SecTrustCopyPublicKey fail");
return nil;
}
return publicKey
}
例えば、リソースとして public-key.der
というファイルをプロジェクトに組み込んでいる場合は、以下のようにして読み込むことができます。
let publicKeyPath = NSBundle.mainBundle().pathForResource("public-key", ofType: "der")!
let publicKey: SecKey? = loadPublicKey(publicKeyPath)
OpenSSLで生成した秘密鍵を読み込む
公開鍵と同じようにして秘密鍵を読み込みたいところですが、OpenSSLで生成した秘密鍵(DER/PEM)を直接読み込むことができません。
Security Frameworkでは、SecIdentity
というデータ型に対してSecIdentityCopyPrivateKey()
関数を適用することで秘密鍵を取り出すことができるのですが、コピー元のSecIdentity
をファイルから作る方法がよくわからず少し苦労しました。いろいろ調べたところ、秘密鍵を 個人情報交換ファイル に変換することで読み込ませることができました。
以下の手順で個人情報交換ファイルを生成することができます。なお、個人情報交換ファイルについての詳細は、PKCS #12 個人情報交換ファイルフォーマットについてというエントリを参照してください。
秘密鍵を PKCS #12形式に変換する
openssl pkcs12
コマンドを使うと、PEMエンコードされた秘密鍵を PKCS #12形式に変換することができます。秘密鍵がDERエンコードされている場合は、事前にPEMエンコードに変換してください。
> openssl pkcs12 -export -in public-key.crt -inkey private-key.pem -out my-identity.p12
Enter Export Password:
Verifying - Enter Export Password:
コマンドを実行するとパスワードの入力が求められます。個人情報交換ファイルは秘密鍵を含むため、全体がこのパスワードで暗号化されて保存されます。このパスワードはプログラムから読み込む際に必要になりますので忘れないようにしてください。
また、上記コマンドでは public-key.crt という公開鍵(証明書)の指定が必要ですので、こちらのエントリなどを参照して、あらかじめ用意しておいてください。証明書は自己署名証明書で構いません。
pkcs12コマンドに -nocertsオプションはPKCS #12ファイルに証明書を含めずに秘密鍵だけを保持させることもできますが、Security Frameworkでは証明書付きのPKCS #12ファイルでないと読み込めないようです。ご注意ください。
PKCS #12形式のファイルから秘密鍵(と公開鍵証明書)を取り出す
以下の手順で、Security Frameworkを使って PKCS #12ファイルで読み込み、秘密鍵を取り出すことができます。
-
SecPKCS12Import()
関数で、PKCS #12ファイルを読み込む- 要素数1のCFArrayがつくられる。ファイルに複数の個人情報を含めている場合は要素数が2以上になる。
- CFArrayから要素を取り出す
- 要素はCFDictionary。
- 取り出した要素(CFDictionary)から、以下のキーで SecIdentityと SecTrustを取り出す
- kSecImportItemIdentity - SecIdentityの取り出すためのキー
- kSecImportItemTrust - SecTrustを取り出すためのキー
- SecIdentityCopyPrivateKey()を使って SecIdentityから SecKey (秘密鍵) を取り出す
- SecTrustCopyPublicKey()を使って SecTrustから SecKey (公開鍵) を取り出す
以下に具体的なコード例を示します。
/**
個人情報交換ファイル(PKCS#12ファイル)から、秘密鍵と公開鍵を読み込みます。
:param: p12KeyPath 個人情報交換(PKCS #12)ファイルのパス
:param: password ファイルを開くためのパスワード
:returns: 読み込まれた秘密鍵。読み込みに失敗した場合はnil
*/
func loadP12Key(p12KeyPath: String, password: String) -> Bool {
let p12KeyFileContent = NSData.dataWithContentsOfMappedFile(p12KeyPath) as NSData?
if (p12KeyFileContent == nil) {
NSLog("Can not read PKCS #12 file from \(p12KeyPath)")
return false
}
// PKCS #12ファイルを読み込み、要素をCFArrayとして取り出す
let options = [String(kSecImportExportPassphrase.takeUnretainedValue()):password]
var citems: Unmanaged<CFArray>? = nil
let resultPKCS12Import = withUnsafeMutablePointer(&citems) { citemsPtr in
SecPKCS12Import(p12KeyFileContent, options, citemsPtr)
}
if (resultPKCS12Import != errSecSuccess) {
return false
}
// 読み込まれた PKCS #12ファイルから SecIdentity(秘密鍵)と SecTrust(証明書)を取り出す
let items = citems!.takeRetainedValue() as NSArray
let myIdentityAndTrust = items.objectAtIndex(0) as NSDictionary
// SecIdentityを取り出す
let identityKey = String(kSecImportItemIdentity.takeUnretainedValue())
let identity = myIdentityAndTrust[identityKey] as SecIdentity
// SecTrustを取り出す
let trustKey = String(kSecImportItemTrust.takeUnretainedValue())
let trust = myIdentityAndTrust[trustKey] as SecTrust
// SecIdentityから 秘密鍵(SecKey)を取り出す
var privateKey: Unmanaged<SecKey>? = nil
let resultCopyPrivateKey = withUnsafeMutablePointer(&privateKey) {
privateKeyPtr in
SecIdentityCopyPrivateKey(identity, privateKeyPtr)
}
if (resultCopyPrivateKey != errSecSuccess) {
NSLog("SecIdentityCopyPrivateKey fail")
return false
}
self.privateKey = privateKey!.takeRetainedValue()
// SecTrustから 公開鍵(SecKey)を取り出す
let publicKey = SecTrustCopyPublicKey(trust).takeRetainedValue() as SecKey?
if (publicKey == nil) {
NSLog("SecTrustCopyPublicKey fail")
return false
}
self.publicKey = publicKey
return true
}
個人情報交換ファイルには公開鍵証明書も含まれているので、これを使えば公開鍵も一緒に読み込むことができます。上記例では SecTrustから 公開鍵も取り出していますが、不要な場合は SecIdentityから秘密鍵だけを取り出せばOKです。
公開鍵(SecKey)を使って文字列を暗号化する
以下のサンプルコードのように、SecKeyEncrypt
を使うとRSAの公開鍵を使った暗号化ができます。
なお、RSAで暗号化できる最大長は鍵の長さに依存するので、それより長い文字列を指定するとエラーになります。
/**
公開鍵を使って文字列を暗号化します。
:param: 暗号化する文字列
:returns: 暗号文。暗号化に失敗した場合はnil
*/
func encrypt(plainString: String) -> NSData? {
// 暗号化に使う公開鍵
var pubKey = self.publicKey
// 文字列をUTF8エンコードのバイト列に変換
let plainBuffer = [UInt8](plainString.utf8)
// 暗号文を格納するためのバッファを用意。
// (暗号化できる最大長は鍵のブロックサイズできまる)
var cipherBufferSize = UInt(SecKeyGetBlockSize(pubKey))
var cipherBuffer = [UInt8](count:Int(cipherBufferSize), repeatedValue:0)
// 暗号化
let status = SecKeyEncrypt(pubKey,
SecPadding(kSecPaddingPKCS1),
plainBuffer,
UInt(plainBuffer.count),
&cipherBuffer,
&cipherBufferSize)
if (status != errSecSuccess){
return nil
}
return NSData(bytes: &cipherBuffer, length: Int(cipherBufferSize))
}
秘密鍵(SecKey)を使って暗号を平文化する
上記関数で作られた暗号は、秘密鍵を使って平文化できます。
/**
秘密鍵を使って暗号を平文化します。
:param: ciphertext 暗号文
:returns: 平文化された文字列
*/
func decrypt(ciphertext: NSData) -> String? {
// 平文化に用いる秘密鍵
var privKey = self.privateKey
// 暗号文に対するUInt8型のポインタ
var cipherBuffer = UnsafePointer<UInt8>(ciphertext.bytes)
// 平文化した文字列を格納するバッファ
var plaintextBufferSize = UInt(SecKeyGetBlockSize(privKey))
var plaintextBuffer = [UInt8](count:Int(plaintextBufferSize), repeatedValue:0)
// 平文化
let status = SecKeyDecrypt(privKey,
SecPadding(kSecPaddingPKCS1),
cipherBuffer,
UInt(ciphertext.length),
&plaintextBuffer,
&plaintextBufferSize)
if (status != errSecSuccess) {
return nil
}
return NSString(bytes: UnsafePointer<Void>(plaintextBuffer), length:Int(plaintextBufferSize), encoding: NSUTF8StringEncoding)
}
暗号化/平文化の動作確認
生成した暗号が平文化できることを確かめてみます。
let plaintext = "hogehoge"
let ciphertext = encrypt(plaintext)!
let decryptedtext = decrypt(ciphertext)!
NSLog("plaintext : \(plaintext)")
NSLog("decryptedtext: \(decryptedtext)")
以下のように同じ文字列が出力されたら成功です。
plaintext : hogehoge
decryptedtext: hogehoge
秘密鍵を使って電子署名を生成する
SecKeyRawSign()
関数を使用すると電子署名を施すことができます。通常、電子署名は対象のデータのダイジェスト値を計算し、それを秘密鍵で暗号化することで作成されます。Javaの暗号化ライブラリなどはダイジェスト値の計算から暗号化までを一貫してやってくれるのですが、SecKeyRawSign()
関数はダイジェスト関数の適用などは呼び出し側が事前に行う必要があります。名前にRawと付いているのはそのあたりからきているのかもしれません。
また、通常の電子署名は PKCS #1の仕様に従ったパディングを施したフォーマットで作成される必要がありますが、SecKeyRawSign()
関数の第二引数に適切なパラメータを与えることで必要なパディングを施すことが可能です。電子署名のフォーマットについては、電子署名のファイルフォーマットというエントリで解説していますので、一読することをお勧めします。
以上を踏まえ、実際に署名を生成するコードは以下のようになります。
/**
秘密鍵を使って平文に署名を施します。
ダイジェスト関数にはSHA1を用い、PKCS #1で定められたフォーマットの署名を出力します。
:param: plainText 署名を施す対象の平文
:returns: PKCS #1で定められたフォーマットの署名。ダイジェスト関数にはSHA1を使用。
*/
func sign(plainText: String) -> NSData? {
// 署名に用いる秘密鍵
var privKey = self.privateKey
// 署名する平文のSHA1ダイジェスト値を計算する
let sha1 = self.getSHA1HashBytes(plainText)
// 署名を格納するバッファ
var signBufferSize = UInt(SecKeyGetBlockSize(privKey))
var signBuffer = [UInt8](count:Int(signBufferSize), repeatedValue:0)
// 署名を行う
let status = SecKeyRawSign(privKey,
SecPadding(kSecPaddingPKCS1SHA1),
UnsafePointer<UInt8>(sha1.bytes),
UInt(sha1.length),
&signBuffer,
&signBufferSize)
if (status != errSecSuccess) {
return nil
}
return NSData(bytes: UnsafePointer<Void>(signBuffer), length: Int(signBufferSize))
}
署名に使用するダイジェスト関数はSHA1を使用しています。SHA1の計算は以下の関数で行っています。
/**
平文の文字列をUTF-8エンコードし、SHA1ダイジェスト値を計算します。
:param: plainText 平文文字列
:returns: SHA1ダイジェスト値
*/
func getSHA1HashBytes(plainText: String) -> NSData {
let plainTextUtf8 = [UInt8](plainText.utf8)
let plainTextData = NSData(bytes: plainTextUtf8, length: plainTextUtf8.count)
return self.getSHA1HashBytes(plainTextData)
}
/**
平文のバイナリ列から SHA1ダイジェスト値を計算します。
:param: plainText 平文バイナリ列
:returns: SHA1ダイジェスト値
*/
func getSHA1HashBytes(plainText: NSData) -> NSData {
// CC_SHA1_CTX構造体を格納するメモリを確保し、ポインタを取得する
var ctx = UnsafeMutablePointer<CC_SHA1_CTX>.alloc(1).move()
// ダイジェスト計算コンテキストを初期化
CC_SHA1_Init(&ctx)
// ハッシュ値の計算
CC_SHA1_Update(&ctx, plainText.bytes, CC_LONG(plainText.length));
// ハッシュ値計算のファイナライズ
var hashBytes = [UInt8](count: Int(CC_SHA1_DIGEST_LENGTH), repeatedValue:0)
CC_SHA1_Final(&hashBytes, &ctx)
return NSData(bytes: &hashBytes, length: hashBytes.count)
}
公開鍵を使用して署名を検証する
署名の検証は、SecKeyRawVerify()
関数で行います。秘密鍵でなく公開鍵を与えるところ以外は SecKeyRawSign()
とほぼ同じです。
/**
署名の検証を行います。用いるダイジェスト関数にはSHA1を使用します。
:param: plainText 対象の平文
:param: signature 署名
:returns: 署名の検証により、改ざんが認められなかった場合 true
*/
func verify(plainText: String, signature: NSData) -> Bool {
// 署名の検証に用いる公開鍵
var pubKey = self.publicKey
// 検証する平文のSHA1ダイジェスト値を計算する
let sha1 = self.getSHA1HashBytes(plainText)
// 署名を検証
let sig = UnsafePointer<UInt8>(signature.bytes)
var sigLen:UInt = UInt(signature.length)
let status = SecKeyRawVerify(publicKey,
SecPadding(kSecPaddingPKCS1SHA1),
UnsafePointer<UInt8>(sha1.bytes),
UInt(sha1.length),
sig,
sigLen)
return status == errSecSuccess
}
電子署名の動作確認
sign関数で生成した電子署名をverify関数で検証する
上記sign関数で生成した電子署名をverify関数で検証してみます。
let plaintext = "hogehoge"
let signature = sign(plaintext) {
let verified = verify(plaintext, signature: signature)
NSLog("verified : \(verified)")
以下のように true と出力されれば検証成功です。
verified : true
Security Frameworkで生成した電子署名をOpenSSLで検証する
OpenSSLとの互換性を確かめるため、上記 sign関数で生成した電子署名を opensslコマンドで検証してみます。
まず、プログラムで生成した電子署名をファイルに書き出す必要があります。ここでは Base64エンコードして一旦文字列化して取り出してみます。
let plaintext = "hogehoge"
if let signature = sign(plaintext) {
// BASE64エンコード
let b64 = signature.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue:0))
NSLog("signature : \(b64)")
} else {
NSLog("Sigining failed")
}
この結果、以下のBase64文字列が得られたので、それをバイナリファイルにもどします。
> echo "n2iScX7ZdTihg6C5YD6yBG7m3umIgiAsHrj/WISPDfhrcIiFio9QKKyz3SMWyKN+Bd9p9T2LZJ5Aww4D94pcieN09/+2Qx+PBjI8L2f7gosT5x/2/647Kgh7/083iMYqgeageVh/cygcckHOv3bKEVc6G/XvfIZWVKm1sCvdEyg=" | base64 -D > signature.dat
openssl dgst
コマンドで検証してみます。
> echo -n "hogehoge" | openssl dgst -sha1 -verify public-key.pem -signature signature.dat
Verified OK
正常に検証できました。
OpenSSLで生成した電子署名を Security Frameworkで検証する
以下のようにして逆も行うことができます。まず、openssl dgst
コマンドで電子署名を生成します。
> echo -n "hogehoge" | openssl dgst -sha1 -sign private-key.pem | base64
n2iScX7ZdTihg6C5YD6yBG7m3umIgiAsHrj/WISPDfhrcIiFio9QKKyz3SMWyKN+Bd9p9T2LZJ5Aww4D94pcieN09/+2Qx+PBjI8L2f7gosT5x/2/647Kgh7/083iMYqgeageVh/cygcckHOv3bKEVc6G/XvfIZWVKm1sCvdEyg=
得られた Base64文字列をプログラムに取り込んで検証してみます。
let b64 = "n2iScX7ZdTihg6C5YD6yBG7m3umIgiAsHrj/WISPDfhrcIiFio9QKKyz3SMWyKN+Bd9p9T2LZJ5Aww4D94pcieN09/+2Qx+PBjI8L2f7gosT5x/2/647Kgh7/083iMYqgeageVh/cygcckHOv3bKEVc6G/XvfIZWVKm1sCvdEyg="
if let signature = NSData(base64EncodedString: b64, options: NSDataBase64DecodingOptions(rawValue: 0)) {
let verified = verify(plaintext, signature: signature)
NSLog("verified: \(verified)")
}
verified: true
と表示されれば検証成功です。
SHA1以外のダイジェスト関数を使用する場合の注意点
SHA1以外のダイジェスト関数(SHA512など)を使用したい場合は、上記コードでCC_SHA1_xx
関数の代わりに、CC_SHA512_xx
関数などを使用してください。
ダイジェスト関数を変更した場合、SecKeyRawSignやSecKeyRawVerifyの第二引数のパディング指定を kSecPaddingPCKS1SHA512
などに変え、使用したダイジェスト関数と一致するようにしてください。パディング指定は signと verifyで揃っていれば動作してしまいますが、OpenSSLなど他の暗号化ライブラリと署名の互換性がなくなってしまうので注意が必要です。
デバッグ時は OpenSSLをリファレンス実装として、暗号化や電子署名が相互に互換性が保たれていることを確認するようにすると安全です。