由上一篇文章咱們能夠知道,公鑰是(e,n)、私鑰是(d,n)。而在實際應用中,咱們接觸到到的不是e、d、n,而是特定格式的數據或者文件。html
PKCS
全稱是 Public-Key Cryptography Standards(公鑰加密標準),是由 RSA 實驗室與其它安全系統開發商爲促進公鑰密碼的發展而制訂的一系列標準,PKCS 目前共發佈過 15 個標準。其中比較經常使用的有:git
標準 | 名稱 | 格式 | 簡介 |
---|---|---|---|
PKCS#1 | RSA密碼編譯標準 | / | 定義了RSA的數理基礎、公/私鑰格式,以及加/解密、籤/驗章的流程。 |
PKCS #7 | 密碼消息語法標準 | / | 參見RFC 2315。規範了以公開密鑰基礎設施(PKI)所產生之簽名/密文之格式。其目的同樣是爲了拓展數字證書的應用。 |
PKCS#8 | 私鑰消息表示標準 | .p8 | Apache讀取證書私鑰的標準。 |
PKCS#10 | 證書申請標準 | .p10 .csr | 參見RFC 2986。規範了向證書中心申請證書之CSR(certificate signing request)的格式。 |
PKCS#12 | 我的消息交換標準 | .p12 .pfx | 定義了包含私鑰與公鑰證書(public key certificate)的文件格式。私鑰採密碼(password)保護。 |
其中.csr或.certSigningRequest是證書請求格式,拿着這個請求文件向CA獲取簽名過的證書。譬如咱們在配置開發證書時候,先經過鑰匙串生成.csr文件,而後上傳,蘋果根據.csr文件爲咱們生成開發證書。github
pfx,p12文件是二進制格式,同時含私鑰和證書,一般有保護密碼。在鑰匙串中因此能夠展開的證書均可以導出p12。算法
X.509
是常見通用的證書格式。全部的證書都符合爲Public Key Infrastructure (PKI) 制定的 ITU-T X509 國際標準。數組
格式 | 編碼形式 |
---|---|
.der | ASCII |
.pem | Base64 |
.cer | 二進制 |
.crt | 二進制 |
ASN.1格式在RSA密鑰證書中,有舉足輕重的地位。上面咱們提到的因此證書格式p十二、pfx、cer,都是ASN.1格式的。將pem中base64串編碼,獲得的公司鑰實體數據也是ASN.1格式的。安全
在電信和計算機網絡領域,ASN.1(Abstract Syntax Notation One) 是一套標準,是描述數據的表示、編碼、傳輸、解碼的靈活的記法。它提供了一套正式、無歧義和精確的規則以描述獨立於特定計算機硬件的對象結構。bash
關於它的語法數據類型等詳細介紹,請參看這篇文章網絡
咱們來看ASN.1的基本編碼規則。ASN.1編碼的數據大體分爲三個部分,標籤(tag)字段
+長度(Length)字段
+值(Value)字段
數據結構
標籤(tag)字段:關於標籤類別和編碼格式的信息。app
長度(Length)字段:定義內容字段的長度(字節數)。
值(Value)字段:包含實際的數據 。
標籤字段(標頭)表示了不一樣的值的數據類型。常見的標頭有
標籤字段 | 數據類型 | 示例 |
---|---|---|
0x01 | 布爾值 | true表示爲 0x01 01 FF;false表示爲 0x01 01 00 |
0x02 | 整型 | 16位整形數的9表示爲 0x02 02 0009 |
0x03 | 位串(bit string) | |
0x04 | 八位串(octor string) | |
0x05 | 空值 | nil 表示爲 0x05 00 |
0x13(19) | 可打印的ASCII編碼字符串 | |
0x16(22) | ASCII編碼字符串 | |
0x31 | 數組 | [3,5]表示爲 0x31 06 0x02 01 03 0x02 01 05 |
下面咱們來看看,公私鑰匙到底長什麼樣子。n、e、d都是怎樣存放的。
爲了研究方便,咱們先用openssl生成一個1024位的RSA私鑰
openssl genrsa -out private-key-1024.pem 1024
複製代碼
導出公鑰
openssl rsa -in private-key-1024.pem -pubout -out public-key-1024.pem
複製代碼
pem格式包含的是base64編碼的數據。咱們取出其中字符串。而後取出首尾標識符及回車符,base64反編碼獲得ASN.1格式的二進制數據。
公鑰的ASN.1結構爲
RSAPublicKey :: = SEQUENCE{
modulus INTEGER n (模長,正整數)
publicExponent INTEGER e (公鑰指數)
}
複製代碼
咱們取出公鑰字符串,而後base64解碼,獲得34字節數據。他的大體結構以下
0x30 --標頭,0x30表示序列類型
0x81 --內容較長,將用後面1(0x80 - 0b10000000)個字節標識長度
0x9f --包含159個字節長度的內容
0x30 --標頭,0x30表示序列類型
0x0d --數據長度,後面包含13個字節數據
0x06 --標頭,6表示對象標識符
0x09 --9個字節
// oid值 1.2.840.113549.1.1.1 (rsaEncryption)
0x2a 0x86 0x48 0x86 0xf7 0x0d 0x01 0x01 0x01
0x05 0x00 -- null
0x03 --標頭,03表示bitstring位串
0x81
0x8d --141字節長度
0x00 --bitstring開頭
0x30 --標頭,0x30表示
0x81
0x89 --137個字節長度
0x02 --模長n的標頭,2表示整數
0x81
0x81 --129字節
// 模長n的值,129字節存儲,128個有效字節
0x00
0xa0 0x29 0xbf 0xd0 0x38 0xfc 0xeb 0xbb
0xba 0xa9 0x09 0x90 0x7c 0x34 0xeb 0x9b
0xd8 0x61 0x73 0x11 0xd1 0x28 0x49 0x39
0xb8 0x43 0xe1 0xc2 0x1e 0xa2 0x87 0x20
0x19 0x5c 0xf1 0x50 0x88 0xb2 0x63 0xc0
0xd5 0x2b 0x68 0x88 0x52 0x75 0xcd 0xd8
0x26 0xba 0xb4 0x30 0x69 0xe0 0xa4 0xe9
0xe0 0x3d 0xcf 0xbf 0x67 0xa7 0x98 0xb1
0xbe 0x20 0x41 0x73 0x5b 0xe6 0xf0 0x7a
0x92 0x41 0x1b 0x62 0x57 0x47 0x60 0x25
0xbe 0x3b 0x75 0xed 0x46 0x0e 0x61 0x52
0x03 0xa5 0x00 0x59 0x1c 0x3c 0x94 0xd2
0x94 0x16 0xbc 0x08 0x6d 0x4f 0xba 0x86
0xc6 0xfc 0xd2 0x3c 0x79 0xc4 0x99 0x17
0xaf 0xf9 0x5c 0x99 0x50 0xe7 0x28 0x2a
0x42 0xd9 0xc7 0x2e 0xba 0x17 0x9c 0x23
0x02 -- e的標頭
0x03 -- e長度爲3個字節
0x01 0x00 0x01 -- 公鑰指數e
複製代碼
公鑰的pem數據中,主要包含兩部份內容,第一部分是OID值,第二部分是公鑰數據實體。
OID值(Object Identifier 對象標識符)爲1.2.840.113549.1.1.1,它表示PKCS1公鑰加密標識符。
其中,各個數字按順序表示爲
值得注意的是:
0x81&0b10000000=1
,因此0x81不表示長度,而是其後0x81-0b10000000=1
個字節表示長度私鑰的ASN.1結構爲
RSAPrivateKey :: = SEQUENCE{
version Version,
modulus INTEGER, ------ n
publicExponent INTEGER, ------ e
privateExponent INTEGER, ------ d
prime1 INTEGER, ------ p
prime2 INTEGER, ------ q
exponent1 INTEGER, ------ d mod (p -1)
exponent2 INTEGER, ------ d mod (q -1)
coefficient INTEGER, ------- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos ------ OPTIONAL(當version爲0時,不存在;當 version爲1時,必須有)
}
Version :: = INTEGER{ two-prime(0), multi(1)}
複製代碼
值得注意的是私鑰文件裏邊,不可是包含實際有效私鑰(e,n),他還包含公鑰指數,咱們在密鑰生成中用到的p、q,以及其餘一些信息。這也是咱們能夠經過私鑰導出公鑰的緣由。
咱們將上面的到的私鑰字符串base64反編碼以後,的到的數據結構以下:
0x30 --標頭,序列類型
0x82 --後面2個字節表示長度
0x02 0x5c --數據長度45
0x02
0x01
0x00 --版本號version爲0
0x02
0x81
0x81 --129個字節
// 模數n
0x00
0xa0 0x29 0xbf 0xd0 0x38 0xfc 0xeb 0xbb
0xba 0xa9 0x09 0x90 0x7c 0x34 0xeb 0x9b
0xd8 0x61 0x73 0x11 0xd1 0x28 0x49 0x39
0xb8 0x43 0xe1 0xc2 0x1e 0xa2 0x87 0x20
0x19 0x5c 0xf1 0x50 0x88 0xb2 0x63 0xc0
0xd5 0x2b 0x68 0x88 0x52 0x75 0xcd 0xd8
0x26 0xba 0xb4 0x30 0x69 0xe0 0xa4 0xe9
0xe0 0x3d 0xcf 0xbf 0x67 0xa7 0x98 0xb1
0xbe 0x20 0x41 0x73 0x5b 0xe6 0xf0 0x7a
0x92 0x41 0x1b 0x62 0x57 0x47 0x60 0x25
0xbe 0x3b 0x75 0xed 0x46 0x0e 0x61 0x52
0x03 0xa5 0x00 0x59 0x1c 0x3c 0x94 0xd2
0x94 0x16 0xbc 0x08 0x6d 0x4f 0xba 0x86
0xc6 0xfc 0xd2 0x3c 0x79 0xc4 0x99 0x17
0xaf 0xf9 0x5c 0x99 0x50 0xe7 0x28 0x2a
0x42 0xd9 0xc7 0x2e 0xba 0x17 0x9c 0x23
0x02
0x03
0x01 0x00 0x01 --公鑰指數e
0x02
0x81
0x80 --128個字節
// 私鑰質數d
0x79 0x69 0xcc 0xb7 0xbb 0x4b 0xb8 0x24
0x32 0xc7 0x4b 0xb1 0xd5 0x06 0x85 0x09
0x3a 0x49 0xfd 0x62 0x27 0x4d 0x43 0xdd
0x56 0x9b 0x56 0xfb 0xc2 0x1f 0x71 0x11
0xdb 0x48 0x42 0xc2 0xcb 0x2d 0x78 0x43
0x49 0x15 0xc4 0x03 0x7b 0x87 0x44 0x49
0x34 0x6a 0xda 0x87 0xcc 0xeb 0x77 0xf8
0xb7 0x7e 0x04 0x0b 0xd4 0x37 0x0f 0x9f
0x92 0xd6 0x31 0xd7 0x4f 0x90 0xa0 0x8e
0x07 0x1a 0xf7 0x0d 0x79 0x25 0xf6 0x1a
0x0a 0x83 0x6b 0x00 0x33 0xbd 0x32 0x2c
0xb3 0xdd 0x71 0x64 0xb5 0xf8 0xcc 0x9f
0x21 0xc3 0x81 0xad 0xab 0xb0 0x1f 0x92
0x0b 0xed 0x88 0x76 0x6c 0x95 0xc6 0xe2
0xe7 0x28 0x24 0xca 0xa0 0x85 0xc7 0x69
0xc2 0x56 0xa2 0x4d 0x70 0x4b 0x59 0xe9
0x02
0x41 --65字節
// 質數p值,有效64字節
0x00
0xd5 0x5f 0x27 0xc6 0x84 0xf4 0x37 0xda
0xa8 0x10 0x28 0x0f 0x33 0x8f 0x05 0xe7
0xa8 0xd3 0x09 0x7f 0xca 0x71 0xfe 0x86
0xa0 0x95 0xb3 0x21 0x30 0xb8 0xb4 0xcf
0x27 0x89 0x21 0xea 0x6d 0xcd 0xaf 0x34
0x2f 0x6d 0x3b 0x64 0xd6 0x41 0x85 0x74
0x10 0xd1 0x63 0x29 0xaa 0xf2 0x79 0xc0
0x4b 0xed 0x2c 0xf9 0x7b 0x7c 0x43 0x0f
0x02
0x41
// 質數q值,有效64字節
0x00
0xc0 0x29 0x40 0x7a 0x96 0x32 0x89 0xf7
0x97 0xbd 0x76 0xa3 0x6c 0xea 0x1b 0x7d
0xa4 0x23 0xe3 0x3d 0x4e 0x08 0x1a 0x21
0x10 0x48 0x81 0xed 0x29 0x01 0xc5 0xae
0xba 0xb9 0x5f 0x98 0x55 0xf4 0x24 0x9c
0xb0 0x14 0x97 0xde 0x34 0x07 0x4d 0x5e
0x53 0x5b 0x6b 0xc2 0x4d 0xcd 0xaf 0x46
0xde 0x9d 0xb8 0x06 0xfd 0x41 0x05 0xad
......
複製代碼
值得注意的是,p和q都是模長的一半,64字節,512位。私鑰質數d,長度和模長一致,都是128字節,1024位。因爲私鑰指數d很大,因此解密時耗費的計算力是比較大的。
在加密或簽名以前,咱們須要將上面所說的密鑰文件轉化爲咱們的密鑰對象。咱們一般採用系統的Security
框架進行加密,與之對應的。咱們須要讀取密鑰文件並生成SecKey
。
pem是咱們最爲常見的存儲RSA密鑰的文件格式。
導入pem密鑰時咱們須要取出pem中的開始結束標識,再進行base64解密獲得密鑰data。
而後經過data生成SecKey
let keyClass = type == .public ? kSecAttrKeyClassPublic : kSecAttrKeyClassPrivate
let sizeInBits = data.count * 8
let keyDict: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: keyClass,
kSecAttrKeySizeInBits: NSNumber(value: sizeInBits),
kSecReturnPersistentRef: true
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateWithData(data as CFData, keyDict as CFDictionary, &error) else {
print(error?.takeRetainedValue() ?? "unkown error")
return nil
}
複製代碼
公私鑰惟一區別是,KeyClass,公鑰時傳kSecAttrKeyClassPublic
,而私鑰是kSecAttrKeyClassPrivate
咱們從文件讀取p12文件,獲得數據data,而後再用data建立SecKey
var item = CFArrayCreate(nil, nil, 0,nil)
let options = pwd != nil ? [kSecImportExportPassphrase:pwd] : [:]
let status = SecPKCS12Import(data as CFData,options as CFDictionary,&item)
if status != noErr {
return nil
}
guard let itemArr = item as? [Any],
let dict = itemArr.first as? [String:Any],
let secIdentity = dict[kSecImportItemIdentity as String] else{
return nil
}
let secIdentityRef = secIdentity as! SecIdentity
var keyRef : SecKey?
SecIdentityCopyPrivateKey(secIdentityRef,&keyRef)
複製代碼
上述代碼中的keyRef
就是咱們獲取到的私鑰對象。
由於私鑰中包含了公鑰的因此信息,咱們也能夠經過私鑰keyRef
導出公鑰
let pubKey1 = SecKeyCopyPublicKey(keyRef)
複製代碼
但這樣作是毫無心義的,由於當咱們拿到p12/pfx時,就意味着咱們拿到的是私鑰。對於客戶端來講是要拿來最數據簽名的。若是要作數據加密,咱們拿到得將是指包含公鑰的pem文件。
在進行RSA加密以前,咱們還須要理解一個重要的概念:padding
爲了提升RSA加密的安全性,加密以前每每會在明文前面加上一段包含隨機數的padding。加入padding以後的數據結構以下:
EM = 0x00 || 0x02 || PS || 0x00 || M.
複製代碼
咱們知道RSA是分塊加密的,而若是有padding,每塊還必須減去一部分長度
padding 方式 | 模長(字節) | 每段明文最大長度(字節) | 每段密文長度 |
---|---|---|---|
no padding | n | n | n |
PKCS1 | n | n-11 | n |
OAEP | n | n-42 | n |
先獲取到模長,根據padding計算分塊最大長度
// 模長
let blockSize = SecKeyGetBlockSize(key)
// 數據分塊的最大長度
var maxChunkSize : Int
switch padding {
case .PKCS1:
maxChunkSize = blockSize - 11
case .OAEP:
maxChunkSize = blockSize - 42
case []: // no padding
maxChunkSize = blockSize
default: // default PKCS1
maxChunkSize = blockSize - 11
}
複製代碼
對數據進行分塊加密
var retData = Data()
var idx = 0
while idx < data.count {
let endIdx = min(idx+maxChunkSize,data.count)
var chunkData = [UInt8](data[idx..<endIdx])
var outLen = blockSize;
let outBuf = UnsafeMutablePointer<UInt8>.allocate(capacity:outLen)
defer { outBuf.deallocate() }
var status = noErr;
status = SecKeyEncrypt(key,
padding,
&chunkData,
chunkData.count,
outBuf,
&outLen)
guard status == noErr else {
print("SecKeyEncrypt fail. Error Code: \(status)")
return nil;
}
retData.append(UnsafeBufferPointer(start:outBuf, count:outLen))
idx += maxChunkSize
}
複製代碼
其中,核心方法是SecKeyEncrypt
。輸入公鑰key,padding類型,以及當前分塊數據chunkData
,輸出outBuf
。
須要注意的是,須要同時知足如下三點,不然加密失敗。
還有就是爲了傳輸方便,通常會將data轉化爲base64字符串
let ret = retData.base64EncodedString()
複製代碼
值得注意的是,因爲padding的存在,咱們對同一數據進行屢次加密,每次加密獲得的結果都是不同的。可是這並不會影響解密的結果,由於padding後的數據結構是固定的,成功解密以後會自動去除無效的數據。
咱們拿到的加密數據,通常是base64字符串。咱們須要先將其轉化爲data再base64解碼
let data = Data(base64Encoded:string, options:.ignoreUnknownCharacters)
複製代碼
跟加密相似的,解碼咱們用到SecKeyDecrypt
方法,具體實現以下:
let blockSize = SecKeyGetBlockSize(key)
var retData = Data()
var idx = 0
while idx < data.count {
let endIdx = min(idx+blockSize,data.count)
var chunkData = [UInt8](data[idx..<endIdx])
var outLen = blockSize;
let outBuf = UnsafeMutablePointer<UInt8>.allocate(capacity:outLen)
defer { outBuf.deallocate() }
var status = noErr;
status = SecKeyDecrypt(key,
padding,
&chunkData,
chunkData.count,
outBuf,
&outLen)
guard status == noErr else {
print("SecKey decrypt fail. Error Code: \(status)")
return nil;
}
let ret1 = UnsafeBufferPointer(start:outBuf, count:outLen)
retData.append(ret1)
idx += blockSize
}
複製代碼
值得注意的是:
outLen
雖然是inout的值,解碼完以後會變成實際獲得的明文長度。但它的初始值不能小於明文長度,不然解密失敗。咱們取模長值是最穩妥的作法。通常在作數字簽名是,每每不是直接用私鑰對明文進行簽名。而是將明文進行模中散列函數運算後,對消息摘要進行簽名。
在驗證的時候,若是用公鑰可以對簽名進行解密,說明發送者身份沒有被仿冒。而後對明文進行散列函數運算獲得的摘要與解密的摘要對比,若是一致證實消息和消息摘要在傳送過程當中都沒有被串改。
不一樣的散列函數,對應不一樣的padding值
散列函數 | 簽名算法 | pading |
---|---|---|
MD5 | MD5WithRSA | PKCS1MD5 |
SHA1 | SHA1WithRSA | PKCS1SHA1 |
SHA224 | SHA224WithRSA | PKCS1SHA224 |
SHA256 | SHA256WithRSA | PKCS1SHA256 |
SHA384 | SHA384WithRSA | PKCS1SHA384 |
SHA512 | SHA512WithRSA | PKCS1SHA512 |
先對原始數據進行散列函數運算
var digestData : Data
switch pading {
case .PKCS1MD5:
digestData = DigestUtil.md5(data:data)
case .PKCS1SHA1:
digestData = DigestUtil.sha1(data:data)
case .PKCS1SHA1:
digestData = DigestUtil.sha1(data:data)
case .PKCS1SHA224:
digestData = DigestUtil.sha224(data:data)
case .PKCS1SHA256:
digestData = DigestUtil.sha256(data:data)
case .PKCS1SHA384:
digestData = DigestUtil.sha384(data:data)
case .PKCS1SHA512:
digestData = DigestUtil.sha512(data:data)
default:
digestData = data
}
複製代碼
對消息摘要進行簽名
let blockSize = SecKeyGetBlockSize(key)
var maxChunkSize : Int = blockSize - 11
var retData = Data()
var idx = 0
while idx < digestData.count {
let endIdx = min(idx+maxChunkSize,digestData.count)
var chunkData = [UInt8](digestData[idx..<endIdx])
var outLen = SecKeyGetBlockSize(key);
let outBuf = UnsafeMutablePointer<UInt8>.allocate(capacity:outLen)
defer { outBuf.deallocate() }
var status = noErr;
status = SecKeyRawSign(key,
pading,
&chunkData,
chunkData.count,
outBuf,
&outLen)
if status == noErr {
let ret1 = UnsafeBufferPointer(start:outBuf, count:outLen)
retData.append(ret1)
}else {
print("SecKey sign fail. Error Code: \(status)")
return nil;
}
idx += maxChunkSize
}
複製代碼
能夠看到它和加密的實現是很像的,並且還採用了和PKCS1相似的padding
standard ASN.1 padding will be done, as well as PKCS1 padding
能夠發現還有兩種SecPadding
咱們沒有提到,sigRaw
和PKCS1MD2
。sigRaw
是DSA算法的,使用的不多。PKCS1MD2
安全性很低,基本已沒人使用。
須要注意的是,因爲散列運算以後的結果都是一致的,而即使是長度最大的SHA512也只有64個字節,遠遠小於RSA簽名117字節的最大塊長度。因此咱們獲得的結果,都是128字節。而且屢次簽名獲得的結果都是一致的。
驗證以前咱們先對原始數據進行與簽名相同散列運算,獲得摘要digestData
var digestData : Data
switch pading {
case .PKCS1MD5:
digestData = DigestUtil.md5(data:data)
case .PKCS1SHA1:
digestData = DigestUtil.sha1(data:data)
case .PKCS1SHA1:
digestData = DigestUtil.sha1(data:data)
case .PKCS1SHA224:
digestData = DigestUtil.sha224(data:data)
case .PKCS1SHA256:
digestData = DigestUtil.sha256(data:data)
case .PKCS1SHA384:
digestData = DigestUtil.sha384(data:data)
case .PKCS1SHA512:
digestData = DigestUtil.sha512(data:data)
default:
digestData = data
}
複製代碼
而後咱們輸入公鑰、padding、明文摘要、簽名,獲得驗證是否成功的結果。
var digestBuf = [UInt8](digestData)
let signBuf = [UInt8](signData)
var status = noErr;
status = SecKeyRawVerify(key,
pading,
&digestBuf,
digestBuf.count,
signBuf,
signBuf.count)
if status == errSecSuccess {
return true
} else {
return false
}
複製代碼
能夠看到,代碼中不存在循環語句。由於簽名數據、摘要數據都是固定長度,而且小於等於模長。因此沒有分段驗證的說法。
若是想看完整的實現,請看這裏