iOS 用 RSA 保證下載資源可靠性
前言
有時需要在本地存儲資源,并且從服務(wù)器下載資源,因為涉及到運行期間的安全性,有必要添加校驗的邏輯,因此有了本文的一些思考。
ipa包被篡改的情況
首先思考的是ipa包的安全性問題。通過iTunes,我們可以下載ipa并且解壓,修改包中的文件,再壓縮成ipa包。
-
1、如果開發(fā)者A拿到應(yīng)用P的ipa包,修改其中的任何文件,都會導(dǎo)致簽名失效,ipa包無法安裝。(簽名存放在.app文件的_CodeSignature文件夾) -
2、如果開發(fā)者B拿到應(yīng)用P的ipa包,安裝到自己手機(jī)中,再直接修改Bundle/Application 下的配置文件,此時應(yīng)用P仍舊可以運行。 -
3、如果開發(fā)者C拿到應(yīng)用P的ipa包,修改其中的某些配置文件,用自己的證書重簽名并通過其他渠道發(fā)布出去,ipa包可以正常安裝。
應(yīng)用在正常使用過程中,app包的文件是無法修改的,只有越獄的機(jī)子才會出現(xiàn)情況2;情況3中重簽名的ipa包無法上傳AppStore。真機(jī)app安裝目錄是 var/mobile/Containers/Bundle/Application 沙盒目錄是 var/mobile/Containers/Data/Application 類似的,模擬的安裝目錄同樣在/Bundle下,沙盒在/Data下;
下載資源的驗證
下載的資源存在沙盒目錄,在未越獄的情況下,開發(fā)者并不能修改其中的文件。但是,下載資源通常使用http/https進(jìn)行資源下載,通過使用代理的方式,可以修改下載的資源(除非開啟了https證書校驗)。為了保證下載資源的可靠性,采用了一套基于RSA算法的驗證方案,具體的要點有:
1、開發(fā)者產(chǎn)生一對密鑰:公鑰和私鑰,私鑰保存在配置平臺(后臺),公鑰放到客戶端。
2、當(dāng)文件上傳到配置平臺后,配置平臺對文件進(jìn)摘要(hash)得到md5str,并私鑰對md5str進(jìn)行簽名得到signStr,然后把 文件和signStr下發(fā)給客戶端。
3、客戶端下載文件和signStr,計算文件的摘要(md5)得到md5str,用md5str和公鑰驗證signStr的有效性。
解釋: 非對稱加密算法的計算比較復(fù)雜 ,所以只對摘要(md5值)進(jìn)行加密;
具體的流程圖如下:
iOS的RSA算法
RSA算法的兩種加密方式:
-
公鑰加密,私鑰解密。(一般用于公鑰持有方(客戶端)向私鑰持有者(后臺)發(fā)送消息) -
私鑰加密,公鑰解密。(一般用于簽名和驗證,私鑰加密相當(dāng)于簽名,公鑰解密相當(dāng)于驗證)
蘋果提供的Security.framework,有以下四個方法:
-
SecKeyEncrypt—encrypts a block of data using the specified key.(使用公鑰對數(shù)據(jù)進(jìn)行加密) -
SecKeyDecrypt—decrypts a block of data using the specified key. (使用私鑰對數(shù)據(jù)進(jìn)行解密) -
SecKeyRawSign—signs a block of data using the specified key.(使用私鑰對數(shù)據(jù)簽名) -
SecKeyRawVerify—verifies a signature against a block of data and a specified key. (使用公鑰對數(shù)字簽名進(jìn)行驗證)
類比到OpenSSL,其提供了以下四個接口:
int RSA_public_encrypt(int flen, const unsigned char *from,
unsigned char *to, RSA *rsa,int padding);
int RSA_private_encrypt(int flen, const unsigned char *from,
unsigned char *to, RSA *rsa,int padding);
int RSA_public_decrypt(int flen, const unsigned char *from,
unsigned char *to, RSA *rsa,int padding);
int RSA_private_decrypt(int flen, const unsigned char *from,
unsigned char *to, RSA *rsa,int padding);
因為RSA算法的計算量較大,一般不會直接使用RSA對數(shù)據(jù)進(jìn)行加密,而是對AES的密匙進(jìn)行加密,再用AES對數(shù)據(jù)加密。
RSA算法原理 ,這里有一篇詳細(xì)介紹RSA算法原理的文章。
數(shù)字簽名的保存
拿到后臺下發(fā)的簽名后,就需要保存簽名,可以選擇:保存在文件中、保存到NSUserDefault、保存到數(shù)據(jù)庫等。除此之外,是否可以保存在文件屬性?
寫了一段代碼進(jìn)行測試:
NSMutableDictionary *changedAttrDict = [[NSMutableDictionary alloc] init];
[changedAttrDict setObject:@"loying" forKey:NSFileOwnerAccountName];
[changedAttrDict setObject:@"NSFileGroupOwnerAccountName" forKey:NSFileGroupOwnerAccountName];
[changedAttrDict setObject:[NSDate dateWithTimeIntervalSinceNow:3600] forKey:NSFileCreationDate];
NSError *error;
BOOL ret = [[NSFileManager defaultManager] setAttributes:changedAttrDict ofItemAtPath:encodedDataPath error:&error];
經(jīng)過測試,NSFileCreationDate這個屬性是可以修改的;NSFileGroupOwnerAccountName和NSFileOwnerAccountName不能修改(真機(jī)為@"mobile");模擬器不可以修改兩個屬性,最大的可能性是因為模擬器運行產(chǎn)生的文件,權(quán)限不夠修改文件屬性;createDirectoryAtPath:withIntermediateDirectories:attributes:這個方法同樣有這個限制。
寫入文件屬性還有其他的限制,當(dāng)文件在不同硬盤格式(HFS+ and FAT32)拷貝的時候,文件附帶的屬性可能會消失。
NSFileProtectionKey 是后臺模式下的文件讀寫
為了開發(fā)方便,可以選擇保存到NSUserDefault的方式。
新建NSMutableDictionary,用文件作為key,用FileConfig作為value。FileConfig是驗證相關(guān)的屬性封裝,便于后續(xù)開發(fā)。
1、NSUserDefault所有的屬性最終會寫入Libary/Preference/下的plist文件中,所以NSUserDefault中不能存儲敏感信息。
2、如果遇到錯誤:Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object
那是因為NSUserDefault只能存基本類型,如果dict里面存有自定義類型,需要先轉(zhuǎn)成NSData再存儲;(plist里面沒有結(jié)構(gòu)信息,只有key-value)
iOS接入步驟
上面介紹了基于RSA的下載資源驗證方案,iOS具體的流程如下:
-
后臺上傳資源文件,配置平臺對文件進(jìn)行hash并用私鑰進(jìn)行簽名得到簽名串signature; -
把文件和signature打包成zip包,下發(fā)到客戶端; -
客戶端解壓zip,得到文件和簽名串signature,對文件進(jìn)行hash,加載本地公鑰,把hash值、signature、公鑰傳給Security.framework; -
用Security.framework提供的 SecKeyRawVerify方法對hash值、signature、公鑰進(jìn)行驗證,如果通過則表示文件未修改。
1、zip解壓
iOS平臺上可以使用MiniZipArchive進(jìn)行解壓。
- (BOOL)unzipFile:(NSString *)file toFilePath:(NSString *)unZipFilePath overWrite:(BOOL)overWrite
{
MiniZipArchive *za = [[MiniZipArchive alloc] init];
BOOL success = NO;
if ([za UnzipOpenFile:file]) {
success = [za UnzipFileTo:unZipFilePath overWrite:overWrite];
[za UnzipCloseFile];
}
return success;
}
2、公鑰和私鑰的加載
.der格式和.pem格式:.der格式表示二進(jìn)制編碼,.pem格式表示Base64編碼。
iOS的公鑰需要用.der格式,私鑰需要用.p12格式,這個可以用openssl的指令來轉(zhuǎn)換。(指令見末尾)
加載的時候先用NSData加載密鑰,再用下面的:getPrivateKeyRefWithContentsOfFile: password:方法加載密鑰;getPublicKeyRefrenceFromeData:方法加載公鑰;
//獲取私鑰
- (SecKeyRef)getPrivateKeyRefWithContentsOfFile:(NSData *)p12Data password:(NSString*)password {
if (!p12Data) {
return nil;
}
SecKeyRef privateKeyRef = NULL;
NSMutableDictionary * options = [[NSMutableDictionary alloc] init];
[options setObject: password forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((__bridge CFDataRef) p12Data, (__bridge CFDictionaryRef)options, &items);
if (securityError == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
if (securityError != noErr) {
privateKeyRef = NULL;
}
}
CFRelease(items);
return privateKeyRef;
}
- (SecKeyRef)getPublicKeyRefrenceFromeData:(NSData *)certData {
SecKeyRef publicKeyRef = NULL;
CFDataRef myCertData = (__bridge CFDataRef)certData;
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)myCertData);
if (cert == nil) {
NSLog(@"Can not read certificate ");
return nil;
}
SecPolicyRef policy = SecPolicyCreateBasicX509();
SecCertificateRef certArray[1] = {cert};
CFArrayRef myCerts = CFArrayCreate(NULL, (void *)(void *)certArray, 1, NULL);
SecTrustRef trust;
OSStatus status = SecTrustCreateWithCertificates(myCerts, policy, &trust);
if (status != noErr) {
NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %d", (int)status);
CFRelease(cert);
CFRelease(policy);
CFRelease(myCerts);
return nil;
}
SecTrustResultType trustResult;
status = SecTrustEvaluate(trust, &trustResult);
if (status != noErr) {
NSLog(@"SecTrustEvaluate fail. Error Code: %d", (int)status);
CFRelease(cert);
CFRelease(policy);
CFRelease(trust);
CFRelease(myCerts);
return nil;
}
publicKeyRef = SecTrustCopyPublicKey(trust);
CFRelease(cert);
CFRelease(policy);
CFRelease(trust);
CFRelease(myCerts);
return publicKeyRef;
}
3、私鑰簽名和公鑰驗證
加載完公鑰和私鑰之后,用私鑰可以對原始數(shù)據(jù)進(jìn)行簽名,詳見PKCSSignBytesSHA256withRSA方法,返回的是簽名串;
在用zip解壓出來的簽名串進(jìn)行驗證的時候,需要用本地的公鑰、原始數(shù)據(jù)和簽名串進(jìn)行驗簽,詳見PKCSVerifyBytesSHA256withRSA方法;
注意的是,因為選擇的算法是kSecPaddingPKCS1SHA256,需要對原始數(shù)據(jù)進(jìn)行一次SHA256的hash。(kSecPaddingPKCS1SHA256只能用于SecKeyRawSign/SecKeyRawVerify)
BOOL PKCSVerifyBytesSHA256withRSA(NSData* plainData, NSData* signature, SecKeyRef publicKey)
{
if (!plainData || !signature) { // 保護(hù)
return NO;
}
size_t signedHashBytesSize = SecKeyGetBlockSize(publicKey);
const void* signedHashBytes = [signature bytes];
size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH;
uint8_t* hashBytes = malloc(hashBytesSize);
if (!CC_SHA256([plainData bytes], (CC_LONG)[plainData length], hashBytes)) {
return NO;
}
OSStatus status = SecKeyRawVerify(publicKey,
kSecPaddingPKCS1SHA256,
hashBytes,
hashBytesSize,
signedHashBytes,
signedHashBytesSize);
return status == errSecSuccess;
}
NSData* PKCSSignBytesSHA256withRSA(NSData* plainData, SecKeyRef privateKey)
{
size_t signedHashBytesSize = SecKeyGetBlockSize(privateKey);
uint8_t* signedHashBytes = malloc(signedHashBytesSize);
memset(signedHashBytes, 0x0, signedHashBytesSize);
size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH;
uint8_t* hashBytes = malloc(hashBytesSize);
if (!CC_SHA256([plainData bytes], (CC_LONG)[plainData length], hashBytes)) {
return nil;
}
SecKeyRawSign(privateKey,
kSecPaddingPKCS1SHA256,
hashBytes,
hashBytesSize,
signedHashBytes,
&signedHashBytesSize);
NSData* signedHash = [NSData dataWithBytes:signedHashBytes
length:(NSUInteger)signedHashBytesSize];
if (hashBytes)
free(hashBytes);
if (signedHashBytes)
free(signedHashBytes);
return signedHash;
}
4、簽名串的保存
簽名串可以使用setxattrf寫入文件的擴(kuò)展屬性,保證簽名串和資源的一一對應(yīng)。
-(BOOL)setExtendValueWithPath:(NSString *)path key:(NSString *)key value:(NSData *)value {
ssize_t writelen = setxattr([path fileSystemRepresentation],
[key UTF8String],
[value bytes],
[value length],
0,
0);
return writelen == 0;
}
比較奇怪的是,比較寫入擴(kuò)展屬性之后的文件大小,并沒有發(fā)生較大變化。在特意查詢文檔之后,發(fā)現(xiàn)下面一句話:Space consumed for extended attributes is counted towards the disk quotasof the file owner and file group 原來擴(kuò)展屬性并不是寫入文件,而是由文件系統(tǒng)來保存。
遇到的問題
1、驗證失敗,SecKeyRawVerify返回-9809
經(jīng)常遇到的問題是,配置平臺的簽名在iOS客戶端驗證不通過,可以按照下面的流程檢測:
-
首先是確保兩端的公鑰和私鑰是一對; -
配置平臺簽名完之后,用iOS客戶端的公鑰在本地驗證; -
確認(rèn)兩邊使用的簽名算法設(shè)置參數(shù)一致; -
iOS客戶端用配置平臺的私鑰進(jìn)行簽名,再用公鑰進(jìn)行驗證; -
對比配置平臺的簽名串和iOS的簽名串;
openssl的驗證命令openssl dgst \-sign private_key.pem \-sha256 \-out sign sourceopenssl dgst \-verify rsa_public_key.pem \-sha256 \-signature sign source
如果驗證通過會有文字提示:Verified OK
2、生成證書失敗,openssl X509: 出現(xiàn) Expecting: TRUSTED CERTIFICATE的錯誤
參考這些公鑰和密鑰的openssl生成命令 openssl genrsa \-out private_key.pem 1024openssl req \-new \-key private_key.pem \-out rsaCertReq.csropenssl x509 \-req \-days 3650 \-in rsaCertReq.csr \-signkey private_key.pem \-out rsaCert.crtopenssl x509 \-outform der \-in rsaCert.crt \-out public_key.deropenssl pkcs12 \-export \-out private_key.p12 \-inkey private_key.pem \-in rsaCert.crt
參考自GithubGist
總結(jié)
任何手段都無法完全防止惡意的攻擊,只能提高門檻。RSA不僅是可以保證資源不被篡改,也可以作為一種驗證,檢查資源是否因為各種原因出現(xiàn)的文件缺失。
附錄
iOS使用Security.framework進(jìn)行RSA 加密解密簽名和驗證簽名
http://www.cnblogs.com/cocoajin/p/6183443.html
http://blog.methodname.com/da-zao-yin-xing-ji-jia-mi/
Signing and Verifying on iOS using RSA
https://stackoverflow.com/questions/21724337/signing-and-verifying-on-ios-using-rsa
xattr manpages
http://manpages.ubuntu.com/manpages/xenial/man7/xattr.7.html
demo地址
https://github.com/loyinglin/LearnRSA
轉(zhuǎn)自:掘金 落影
https://juejin.cn/post/6969379271248707621
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
