由上一篇文章咱们能够知道,公钥是(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
}
复制代码
能够看到,代码中不存在循环语句。由于签名数据、摘要数据都是固定长度,而且小于等于模长。因此没有分段验证的说法。
若是想看完整的实现,请看这里