PKCS
PKCS(Public Key Cryptography Standards, PKCS)公鑰加密標準,是美國RSA信息安全公司旗下的RSA實驗室開發的一系列編譯標準,非對稱密鑰一般都包含其他信息,所以PKCS通過ASN.1的格式標準定義密鑰展示
一個PKCS#1 公鑰用asn.1表示格式如下:
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}
modulus就是n,publicExponent就是e,n和e就代表了公鑰。上面asn.1格式的標準
一個PKCS#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
}
openssl默認使用的是PKCS#1,但這個已經非常舊了,openssl主要是為了兼容,推進使用PKCS#8
PKCS#8是一個專門用于編碼私鑰的標準,可用于編碼 DSA/RSA/ECC 私鑰。它通常被編碼成 PEM 格式存儲。相比較PKCS#1,它比較安全可以兼容任何格式的私鑰,因此建議用PKCS#8來代替
X.509
X.509是密碼學里公鑰證書的格式標準。比如ssl用的就是它
x.509是公鑰標準,基本上現在的庫公鑰都使用x.509,私鑰標準符合pkcs。pkcs#8相比較在pkcs#1的標準上增加了一些頭部信息,比pkcs#1安全性高
X.509的RSA公鑰格式:
RSAPublicKey ::= SEQUENCE {
algorithm AlgorithmIdentifier , // 這就是增加的頭信息
publicKey RSAPublicKey // 這就是PKCS#1的RSA公鑰的內容
}
PKCS#8的RSA私鑰格式:
PrivateKey ::= SEQUENCE {
version Version , // 這就是增加的頭信息
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier , // 這也是增加的頭信息,表示使用的什么算法,可以是 RSA,也可以是其它的算法,比如 DES、AES 等對稱加密算法等。
privateKey RSAPrivateKey // 這就是PKCS#1的RSA私鑰的內容
}
上面的公鑰用der編碼得到二進制格式,而為了方便看再用base64編碼就是pem格式的字符串了。
PEM 和 DER編碼
ASN.1通過DER編碼把公鑰和私鑰編碼成二進制格式以便于網絡上傳輸而PEM則是為了方便,對DER進行base64編碼同時在頭和尾處加上一行字符串進行標記PEM格式,這樣字符串就比較方便復制查看
pkcs#1的例子用pem編碼后的格式如下:
// 公鑰
-----BEGIN RSA PUBLIC KEY-----
BASE64編碼的DER密鑰文本
-----END RSA PUBLIC KEY-----
// 私鑰
-----BEGIN RSA PRIVATE KEY-----
BASE64編碼的DER密鑰文本
-----END RSA PRIVATE KEY-----
pkcs#8編碼后的未加密的私鑰格式:
-----BEGIN PRIVATE KEY-----
BASE64編碼的DER密鑰文本
-----END PRIVATE KEY-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
BASE64編碼的DER密鑰文本
-----END ENCRYPTED PRIVATE KEY-----
x.509的公鑰編碼后的格式:
-----BEGIN PUBLIC KEY-----
BASE64編碼的DER密鑰文本
-----END PUBLIC KEY-----
相比較pkcs#1,就少了個rsa字符
通常以DER格式存儲的證書,大都使用 .cer
.crt
.der
拓展名,在 Windows 系統比較常見,而PEM 格式的數據通常以 .pem
.key
.crt
.cer
等拓展名存儲,打開查看就是一堆字符串,openssl 默認使用的就是pem格式。
pkcs填充規則
在rsa加密的過程中,密文的長度不能大于密鑰的長度,也就是必須滿足
0 < m < n
,如果長了則需要對數據進行分段加密,但是如果m太短則需要對m進行填充
rsa加密的密文m是不能超過密鑰的長度的,如果m>n,該公式就不能成立 m=pow(y, d) % n
無法解密,運算就會出錯。
填充規則常用的標準有NoPPadding,OAEPPadding,PKCS1Padding這幾種,go 在crypto/rsa
庫中用的是PKCS #1 v1.5 padding,PKCS1Padding的填充總共占用11個字節,對于1024位長度的密鑰占用128個字節,減去11個字節,那明文最長的長度就是128-11=117個字節。1024長度的被破解過已經不建議使用了,至少使用2048或以上長度的密鑰比較安全。PKCS1Padding 8.1 Encryption-block formatting填充規則如下:
// M為明文
// BT 代表block type塊類型,有0x00,0x01,0x02, 如是是私鑰則BT=00x0或01x0。如果是公鑰操作,BT=0x02。
// PS為填充的字節,BT=0x00則PS=0x00,BT=0x01則PS=0xFF,BT=0x02則PS=非0偽隨機數
EM = 0x00 || BT || PS || 0x00 || M
// 假設密鑰長度是2048,也就是256個字節,BT=0x02,M = 100個字節,則PS = 256 - 100 - 3 字節,填充的結構如下:
em = 0x00 + 0x02 + (256 - 100 - 3)字節的隨機數 + 0x00 + m
go 在crypto/rsa
中的公鑰填充加密代碼示例
// https://pkg.go.dev/crypto/rsa#EncryptPKCS1v15
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
randutil.MaybeReadByte(rand)
if err := checkPub(pub); err != nil {
return nil, err
}
k := pub.Size()
if len(msg) > k-11 {
return nil, ErrMessageTooLong
}
// EM = 0x00 || 0x02 || PS || 0x00 || M
em := make([]byte, k)
em[1] = 2
ps, mm := em[2:len(em)-len(msg)-1], em[len(em)-len(msg):]
err := nonZeroRandomBytes(ps, rand)
if err != nil {
return nil, err
}
em[len(em)-len(msg)-1] = 0
copy(mm, msg)
m := new(big.Int).SetBytes(em)
c := encrypt(new(big.Int), pub, m)
return c.FillBytes(em), nil
}
func encrypt(c *big.Int, pub *PublicKey, m *big.Int) *big.Int {
e := big.NewInt(int64(pub.E))
// 這里就是加密的過程了,就是上面我們說的公式pow(m,e)%n,不寫第三個參數,可以單獨調用Mod取模算出最終加密結果
c.Exp(m, e, pub.N)
return c
}
func (x *Int) FillBytes(buf []byte) []byte {
// Clear whole buffer. (This gets optimized into a memclr.)
for i := range buf {
buf[i] = 0
}
x.abs.bytes(buf)
return buf
}
簽名
簽名就是用私鑰加密,而驗簽是用公鑰解密。簽名的目的是為了證明發出消息的人以及消息是否完整,擁有私有簽名的數據,則只有持有公鑰的人才可以解開
簽名分為以下幾步:
- 對數據進行哈希運算得到一個短的哈希值,因為rsa加密有長度限制。
h= hash(m)
- 對哈希值和摘要算法標識符OID進行asn.1編碼
DigestInfo ::= SEQUENCE { digestAlgorithm DigestAlgorithmIdentifier, // 消息摘要算法 digest Digest // 就是哈希運算的結果 h }
- der編碼后對數據進行填充然后利用私鑰進行加密,和上面加密的填充過程一樣,區別是這次是用私鑰,填充的塊類型BT和PS有些區別
EM = 0x00 || 0x01 || PS || 0x00 || T // 以上面的例子為參考,2048長度的密鑰,密文的長度最多256個字節,假設len(m)=100,m為der編碼后的數據 em = 0x00 + 0x01 + (256 - 100 - 3)字節的0xff + 0x00 + m
- 私鑰加密
go 在crypto/rsa
中的簽名,驗簽代碼示例
// 簽名
func SignPKCS1v15(rand io.Reader, priv *PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) {
hashLen, prefix, err := pkcs1v15HashInfo(hash, len(hashed))
if err != nil {
return nil, err
}
tLen := len(prefix) + hashLen
k := priv.Size()
if k < tLen+11 {
return nil, ErrMessageTooLong
}
// EM = 0x00 || 0x01 || PS || 0x00 || T
em := make([]byte, k)
em[1] = 1
for i := 2; i < k-tLen-1; i++ {
em[i] = 0xff
}
copy(em[k-tLen:k-hashLen], prefix)
copy(em[k-hashLen:k], hashed)
m := new(big.Int).SetBytes(em)
c, err := decryptAndCheck(rand, priv, m)
if err != nil {
return nil, err
}
return c.FillBytes(em), nil
}
// 驗簽
// VerifyPKCS1v15 verifies an RSA PKCS #1 v1.5 signature.
// hashed is the result of hashing the input message using the given hash
// function and sig is the signature. A valid signature is indicated by
// returning a nil error. If hash is zero then hashed is used directly. This
// isn't advisable except for interoperability.
func VerifyPKCS1v15(pub *PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error {
hashLen, prefix, err := pkcs1v15HashInfo(hash, len(hashed))
if err != nil {
return err
}
tLen := len(prefix) + hashLen
k := pub.Size()
if k < tLen+11 {
return ErrVerification
}
// RFC 8017 Section 8.2.2: If the length of the signature S is not k
// octets (where k is the length in octets of the RSA modulus n), output
// "invalid signature" and stop.
if k != len(sig) {
return ErrVerification
}
c := new(big.Int).SetBytes(sig)
m := encrypt(new(big.Int), pub, c)
em := m.FillBytes(make([]byte, k))
// EM = 0x00 || 0x01 || PS || 0x00 || T
ok := subtle.ConstantTimeByteEq(em[0], 0)
ok &= subtle.ConstantTimeByteEq(em[1], 1)
ok &= subtle.ConstantTimeCompare(em[k-hashLen:k], hashed)
ok &= subtle.ConstantTimeCompare(em[k-tLen:k-hashLen], prefix)
ok &= subtle.ConstantTimeByteEq(em[k-tLen-1], 0)
for i := 2; i < k-tLen-1; i++ {
ok &= subtle.ConstantTimeByteEq(em[i], 0xff)
}
if ok != 1 {
return ErrVerification
}
return nil
}
func pkcs1v15HashInfo(hash crypto.Hash, inLen int) (hashLen int, prefix []byte, err error) {
// Special case: crypto.Hash(0) is used to indicate that the data is
// signed directly.
if hash == 0 {
return inLen, nil, nil
}
hashLen = hash.Size()
if inLen != hashLen {
return 0, nil, errors.New("crypto/rsa: input must be hashed message")
}
prefix, ok := hashPrefixes[hash]
if !ok {
return 0, nil, errors.New("crypto/rsa: unsupported hash function")
}
return
}
// For performance, we don't use the generic ASN1 encoder. Rather, we
// precompute a prefix of the digest value that makes a valid ASN1 DER string
// with the correct contents.
var hashPrefixes = map[crypto.Hash][]byte{
crypto.MD5: {0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, 0x10},
crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14},
crypto.SHA224: {0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0x04, 0x1c},
crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20},
crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30},
crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40},
crypto.MD5SHA1: {}, // A special TLS case which doesn't use an ASN1 prefix.
crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14},
}
go在hashPrefixes
里提前計算好了沒個哈希算法標識符的der編碼后的值,注釋里說是為了提升性能。我以sha256為例,實現如下,發現除了首尾4個字節,中間部分一致:
package main
import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"fmt"
)
func main() {
// hash256算法標識oid
oidSHA256 := asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
mgf1Params := pkix.AlgorithmIdentifier{
Algorithm: oidSHA256,
Parameters: asn1.NullRawValue,
}
d, err := asn1.Marshal(mgf1Params)
if err != nil {
fmt.Println(err)
}
oid := hex.EncodeToString(d)
fmt.Println(oid)
// 輸出如下:
// 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00
// {0x30, 0x31, 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}
}
可以看到輸出的和上面hashPrefixes
里sha256的值除了首尾4個字節不同,首部2個字節分別是0x30,0x31
,尾部2個字節分別是 0x04, 0x20
,至于這4個分別代表什么暫時不清楚,??♂?。
經查閱文檔發現是我構造的簽名數據結構問題,然后我們加上簽名的數據m,按照文檔定義數據結構(參考RFC 2313 10.1.2),發現得到的der編碼前半部分剛好與hashPrefixes
里sha256的值是一樣的,后半部分剛好是哈希值編碼后的值,如果對編碼后的數據進行填充,然后私鑰加密,其實就是實現了一次簽名的完整過程。
/* ASN1 DER structures
DigestInfo ::= SEQUENCE {
digestAlgorithm AlgorithmIdentifier,
digest OCTET STRING
}
*/
// 算法標識符
type AlgorithmIdentifier struct {
Algorithm asn1.ObjectIdentifier
Parameters asn1.RawValue `asn1:"optional"`
}
// 簽名的數據結構
type DigestInfo struct {
DigestAlgorithm AlgorithmIdentifier
Digest []byte
}
sha := sha256.New()
m := []byte{50}
sha.Write(m)
h := sha.Sum(nil)
var digestInfo = DigestInfo{
DigestAlgorithm: AlgorithmIdentifier{
Algorithm: oidSHA256,
Parameters: asn1.RawValue{
Tag: asn1.TagNull,
},
},
Digest: h,
}
d, err := asn1.Marshal(digestInfo)
if err != nil {
fmt.Println(err)
return
}
oid := hex.EncodeToString(d)
fmt.Println(oid)
// 輸出
// 3031300d060960864801650304020105000420 d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35
但是上面代碼如果我們去掉生成哈希值的部分,然后 Digest
字段的值定義為空或者不填寫則生成的值和hashPrefixes[sha256]
是不一樣,這種區別剛好區分在首部2個字節,那么問題來了這2個字節分別代表什么意思呢?這里牽扯到ans.1的der編碼規則,不是很懂,后邊這塊的知識需要再補補,簡單來說0x30指的是類型,代表著一個sequence結構,0x31指的是后邊數據的長度。
asn.1的der編碼規則是遵循了type-length-value
規則,由幾個部分組成
Identifier octets Type | Length octets | Contents octets | End-of-Contents octets |
---|---|---|---|
Type | Length | Value | (only if indefinite form) |
Type用高2位表示Tag class,高位第3位表示是否是復合數據類型P/C,后邊則是 TagNumber
0x30就指的是Type,0x30轉換成二進制 0011 0000
,可以看到前面2個位是0。可以看到 00是tag class,代表asn.1的原生數據類型,1是 P/C C指的是復合數據類型,由于簽名是 SEQUENCE
結構體所以這里是復合數據類型,所以是1,后邊的 1 0000 轉換成10進制是16,而16所在的tagNumber剛好代表 SEQUENCE
,參考x.690 BER encoding Identifier octets
0x31指的是數據長度,長度又分定長和不定長等,說起來就比較多了,詳細的另寫一篇記錄。