赞
踩
由上一篇文章我们可以知道,公钥是(e,n)、私钥是(d,n)。而在实际应用中,我们接触到到的不是e、d、n,而是特定格式的数据或者文件。
PKCS
全称是 Public-Key Cryptography Standards(公钥加密标准),是由 RSA 实验室与其它安全系统开发商为促进公钥密码的发展而制订的一系列标准,PKCS 目前共发布过 15 个标准。其中比较常用的有:
标准 | 名称 | 格式 | 简介 |
---|---|---|---|
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文件为我们生成开发证书。
pfx,p12文件是二进制格式,同时含私钥和证书,通常有保护密码。在钥匙串中所以可以展开的证书都可以导出p12。
X.509
是常见通用的证书格式。所有的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。
格式 | 编码形式 |
---|---|
.der | ASCII |
.pem | Base64 |
.cer | 二进制 |
.crt | 二进制 |
ASN.1格式在RSA密钥证书中,有举足轻重的地位。上面我们提到的所以证书格式p12、pfx、cer,都是ASN.1格式的。将pem中base64串编码,得到的公司钥实体数据也是ASN.1格式的。
在电信和计算机网络领域,ASN.1(Abstract Syntax Notation One) 是一套标准,是描述数据的表示、编码、传输、解码的灵活的记法。它提供了一套正式、无歧义和精确的规则以描述独立于特定计算机硬件的对象结构。
关于它的语法数据类型等详细介绍,请参看这篇文章
我们来看ASN.1的基本编码规则。ASN.1编码的数据大致分为三个部分,标签(tag)字段
+长度(Length)字段
+值(Value)字段
标签(tag)字段:关于标签类别和编码格式的信息。
长度(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
}
可以看到,代码中不存在循环语句。因为签名数据、摘要数据都是固定长度,并且小于等于模长。所以没有分段验证的说法。
如果想看完整的实现,请看这里
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。