密碼學基礎3:常見密鑰格式完全解析

這是密碼學筆記第三篇。之前兩篇分別是分析 RSA 和 橢圓曲線密碼的基本原理,本文分析了常見的密鑰格式,以確保在開發中能夠使用正確格式的密鑰。如有錯誤,懇請指正。

1 ASN.1

ASN.1(Abstract Syntax Notation One) 是用於描述抽象數據類型的一種標記,它最早用於電信領域,後來在計算機密碼學中也有廣泛應用。ASN.1 跟 protobuf 和 Thrift 類似,可以看作是一種接口描述語言,通過定義 scheme 描述數據,當然它出現的要更早。

ASN.1 定義了一些數據類型來描述數據結構,包括基礎類型(如整數,布爾值,字符串類型)和結構化類型(如結構體,列表類型),完整類型列表見 [ASN.1 Types]。除了 CHOICE 和 ANY 類型外,類型通常都有個類型標籤。類型標籤分爲通用的、應用自定義的、上下文特定的、以及私有的類型標籤 4 種。密鑰常用的是通用的類型標籤,如下:

Type Tag Number
INTEGER 0x02
BIT STRING 0x03
OCTET STRING 0x04
NULL 0x05
OBJECT IDENTIFIER 0x06
SEQUENCE and SEQUENCE OF 0x10
IA5String 0x16
UTCTime 0x17

下面用ASN.1定義了一個數據結構 FooQuestion。FooQuestion 包括兩個字段 id (整形) 和 question(IA5String是不包括控制字符的 ASCII 字符串類型)。

FooQuestion ::= SEQUENCE {
    id INTEGER,
    question IA5String
}

ASN.1 只是描述了數據結構,並沒有指定怎麼編碼數據。因此,出現了多種編碼規則以方便數據在網絡上傳輸和不同終端間交互。比較常見的有 XER, JER, BER, DER等。如待編碼的數據如下:

myQuestion FooQuestion ::= SEQUENCE {
    id 5,
    question "Anybody there?"
}

使用各編碼規則編碼結果如下,其中 XER 和 JER 不用多說,BER 和 DER 是最常見的密鑰文件編碼規則,下一節詳細分析。

                    XER(XML Encoding Rules)
            <FooQuestion>
                <id>5</id>
                <question>Anybody there?</question>
            </FooQuestion>

                    JER(JSON Encoding Rules)
    { "id" : 5, "question" : "Anybody there?" }
        
                    BER(Basic Encoding Rules)
        30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f

                    DER(Distinguished Encoding Rules)
        30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f

2 密鑰常見的編碼規則

前文提到 ASN.1 只是定義了數據結構,並未規定具體的編碼方式,於是出現了多種基於 ASN.1 的編碼規則。本節主要介紹密鑰和證書中常見的編碼規則 BER,DER。

Basic Encoding Rules(BER)

BER 是基礎編碼規則,編碼結構包括類型標誌、長度,值以及結束符(可選),每個字段以 8bit 即字節進行分割。

----------------------------------------------------
| Identifier | Length | Contents | End-of-contents |
|   octets   | octets |  octets  |      octets     |
----------------------------------------------------

Identifier octets: 類型標誌 Identifier 就是ASN.1 規定的類型,只是除了標籤號(tag number)外,還加了 3 位,第 7,8 位用於區分是通用的標籤類型還是其他標籤類型, 第 6 位 用於區分是基礎類型還是結構化類型。Identifier 結構如下,後面我們會看到密鑰中的結構體類型 SEQUENCE 的 Identifier 爲 0x30,即是由這個格式而來(0011000)。

---------------------------------
| 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
---------------------------------
|Class  |P/C| Tag Number        |
---------------------------------
  • Class:如果是通用類型標籤則爲 00,應用自定義的類型標籤則爲 01,上下文特定類型標籤 10,私有類型標籤 11。
  • P/C:如果是基礎類型,則爲 0,結構化類型爲 1。
  • Tag Number:就是 ASN.1 中定義的數據類型標籤號。

Length: 分三種情況,

  • 1)數據長度 < 128:則 Length 的 8bit首位爲0,其他7位表示數據長度。
  • 2)數據長度 >= 128:則 Length 的第一個8bit爲 0x8?,其中 ? 是後面跟的是長度。比如 0x81 表示後面一個 8bit 爲長度,如果是 0x82 則表示後面兩個 8bit 爲長度,以此類推。
  • 3)如果數據長度未知,則 Length=0x80,並增加 End-of-contents=00 00 結束標記。

Contents & End-of-contents: 數據內容 Contents 按 8bit 分組,類型和長度由前兩個字段確定。對於未知數據長度的數據類型,纔有 End-of-contents,爲00 00

實例:

  • 使用 OCTET STRING 編碼字符串 Hello,爲 04 05 48 65 6C 6C 6F,即類型爲 04,長度爲 05,內容爲 0x48 65 6C 6C 6F,即 Hello 的 ASCII 碼。
  • 使用 INTEGER 編碼整數 3,爲 02 01 03,原理同上。
  • 結構化類型就是包含了多個簡單類型的複合類型,後面詳細分析。

Distinguished Encoding Rules (DER)

DER 是典型的 Tag-Length-Value(TLV) 編碼方式,是 PKCS 密鑰體系常用的編碼。
DER 是 BER 的子集,編碼規則幾乎一樣,不過去掉了 BER 的一些靈活性,多了幾個限制:

  • 如果數據長度在 0-127 之間,則 Length 必須使用第 1 種編碼方式。
  • 如果數據長度 >= 128,則 Length 必須使用第 2 種編碼方式,且 Length 必須用最少的字節編碼,如果能用 2 字節的則不能用 3 字節。
  • 數據要用明確長度的編碼方式,即不支持 Length 的第3種未知數據長度+結束標記的編碼方式。

因爲 DER 編碼是二進制數據,早期的 Email 不能發送附件,也不方便直接傳輸二進制數據([原因]),因此密鑰文件通常會 在 DER 格式基礎上進行 Base64 編碼,這就是經常看到的密鑰文件格式 PEM。

PEM(Privacy Enhanced Mail): 最早是用來增強郵件安全性的,不過沒有被廣泛接受,最後卻是在密碼學中得到了發揚光大,如 openssl 和 ssh-keygen 工具生成的公私鑰文件默認都採用 PEM 格式。

3 密鑰格式解析

編碼規則只定義了數據編碼方式,但是並沒有賦予數據意義。公鑰密碼學標準 PKCS (Public Key Cryptography Standards) 和公鑰基礎設施 PKIX(Public-Key Infrastructure X.509) 等使用 ASN.1 的 scheme 定義密鑰和證書的格式和編碼,以描述公私鑰和證書屬性。 需要注意,PKCS 雖然名字是公鑰密碼學標準,它其實也包括私鑰格式標準。 這兩個標準的內容十分翰大,本節只分析其中常見的幾種密鑰相關的部分。

3.1 PKCS #1

PKCS #1 是 RSA Cryptography Standard,即 RSA 密碼學標準,它定義了 RSA 公私鑰的格式和屬性,以及加解密、簽名、填充的基礎算法。

RSA 密鑰格式

RSA 公私鑰的 ASN.1 scheme 在 [rfc8017] 定義如下,根據 scheme 和 PEM 編碼的數據,就能解析出 RSA 公私鑰中的參數了(參數含義請參考我之前的《RSA算法原理解析》一文)。


 RSAPublicKey ::= SEQUENCE {
        modulus           INTEGER,  -- n
        publicExponent    INTEGER   -- e
 }
         
 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 生成的一對 RSA 公私鑰 (示例爲方便展示,用的 1024 位密鑰,實際中請使用 2048 位以上)

$ openssl genrsa -out prikey.p1 1024
$ openssl rsa -in prikey.p1 -pubout -RSAPublicKey_out > pubkey.p1

PKCS#1 格式解析如下:公鑰的 SEQUENCE 包括 RSA 公鑰參數 n 和 e 兩個屬性。RSA 私鑰則首先是版本號 0,然後是 RSA 私鑰的 8 個參數。

加密私鑰

可以對 PKCS#1 的私鑰進行加密,如 ssh-keygen 可以指定 passphrase (測試的密碼是 testtest)加密 RSA 私鑰,加密後的私鑰 enc_prikey.p1 格式如下:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,8C2A8D6593F411D7336B842037B5200B

EncryptedRSAPrivateKey
-----END RSA PRIVATE KEY-----

DEK-Info 裏面指明瞭加密算法是 AES-128-CBC,IV 是 8C2A8D6593F411D7336B842037B5200B,AES加密的實際密碼=md5(設定密碼 + IV的前8個字節)。可以使用 openssl aes-128-cbc 驗證加密結果是否與 EncryptedRSAPrivateKey 一致。

$ tail -n +2 prikey.p1 | grep -v 'END RSA' | base64 -d | 
openssl aes-128-cbc -e -iv 8C2A8D6593F411D7336B842037B5200B -K $(python -c "exec(\"import hashlib\\nprint hashlib.md5(bytearray('testtest') + bytearray.fromhex('8C2A8D6593F411D7')).hexdigest()\")") | base64

如果要使用 python 模塊實現 AES 加密,需要將 password 和 iv 都轉換爲 byte 類型,如下所示。

PWD = 'testtest'
IV = '8C2A8D6593F411D7336B842037B5200B'
b_iv = bytes(bytearray.fromhex(iv))
b_key = hashlib.md5((bytearray(pwd) + bytearray.fromhex(iv[:16]))).digest()

openssl 和 openssh 生成公鑰格式區別

使用 ssh-keygen -t rsa 生成的 RSA 密鑰對中雖然私鑰格式跟 PKCS#1 相同(注:openssh 現在也支持一種新的專用的私鑰格式,不兼容其他標準),但是公鑰格式不一樣。ssh-keygen 生成的公鑰如下所示,這不是 PKCS#1 標準格式,而是 openssh 使用的一種專屬格式:

[type-name] [base64-encoded-ssh-public-key] [comment]

如下面這樣:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+gCiA//vUMu/2dYj9oGUpY2TCw5/AtkfI2cWvl7hOkliQd7uI61gE9BV5w+Ib+HnjAB9lFYS4A8rlpRlkH9a+mCN2K/Oh5dhoonxat4qeHB5XDvmImU
fdOGayT5l176KWP4ftGJt+8ygRpo05zcbuBrd/KxFZ7KDiQyXRvRv9mw== vagrant@stretch

解析後可以發現內容就是 e 和 n 兩個參數的 base64 編碼。(注:ASN.1 中規定 INTEGER 類型如果是正數,則最高位須是 0,負數最高位爲 1。因此密鑰參數中如果有值最高位爲 1,則需要在參數前額外加 00 以表示正數,參數 n 前多 00 就是這個原因)。

73 73 68 2d 72 73 61 # ssh-rsa

01 00 01 # e

00 be 80 28 80 ff fb d4 32  ef f6 75 88 fd a0 65 # n
29 63 64 c2 c3 9f c0 b6 47  c8 d9 c5 af 97 b8 4e
......

3.2 PKCS #8

PKCS#8 是 Private-Key Information Syntax Standard,即私鑰格式相關的標準,它不像 PKCS#1 只支持 RSA,而是支持各種類型的私鑰。PKCS#8 私鑰文件格式中首尾並未說明私鑰算法類型,算法類型在數據中標識。PKCS#8 中的私鑰也支持加密。

未加密私鑰格式

未加密私鑰格式的 ASN.1 scheme 定義如下(參見 [rfc5958] ):

    OneAsymmetricKey ::= SEQUENCE {
        version                   Version,
        privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
        privateKey                PrivateKey,
        attributes            [0] Attributes OPTIONAL,
       ...,
       [[2: publicKey        [1] PublicKey OPTIONAL ]],
       ...}

        Version ::= INTEGER  # 版本號。
    
        PrivateKeyAlgorithmIdentifier ::= SEQUENCE  { # 密鑰算法標識
                algorithm               OBJECT IDENTIFIER,
                parameters              ANY DEFINED BY algorithm OPTIONAL  }
    
        PrivateKey ::= OCTET STRING # 不同類型的私鑰格式不同,比如 RSA 的是 RSAPrivateKey類型,而 ECC 的是 ECPrivateKey 類型。
            
        Attributes ::= SET OF Attribute # 跟公鑰相關的屬性,比如證書什麼的,在公私鑰中通常爲空。
        
        PublicKey ::= BIT STRING # 不同類型密鑰包含的公鑰內容也不同。

RSA 私鑰格式

可以使用 openssl 將 PKCS#1 格式的私鑰 prikey.p1 轉換成 PKCS#8 格式的 prikey.p8,如下:

$ openssl pkcs8 -in prikey.p1 -topk8 -out prikey.p8 -nocrypt

私鑰格式解析如下:

  • version:版本號,目前值爲 0。
  • privateKeyAlgorithm:私鑰算法,爲 rsaEncryptionOBJECT IDENTIFIER1.2.840.113549.1.1.1,具體含義參見 [這裏]
  • privateKey:私鑰,OCTET STRING 類型,裏面其實封裝了一個 RSAPrivateKey 類型,跟 PKCS#1 一樣。
  • attributes 和 publicKey 爲空。

ECC 私鑰格式

橢圓曲線類型的私鑰格式在 rfc5915 中定義如下:

ECPrivateKey ::= SEQUENCE {
     version        INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
     privateKey     OCTET STRING,
     parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
     publicKey  [1] BIT STRING OPTIONAL
}

使用 openssl 創建一個 PKCS#8 格式的 ecc 密鑰,採用 prime256v1 曲線:

# 生成傳統格式的 ECC 私鑰,類似 PKCS#1 那樣,只包含 privateKey,密鑰類型在頭部 -----BEGIN EC PRIVATE KEY----- 標識,橢圓曲線在 parameters 標識。
$ openssl ecparam -name prime256v1 -genkey -noout -out ecc_prikey.tradfile

# 轉換爲 PKCS#8 格式
$ openssl pkcs8 -topk8 -in ecc_prikey.tradfile -out ecc_prikey.p8 -nocrypt
$ cat ecc_prikey.p8
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgMbahscIGpSZ6NULI
iQ/pTI9ZcvFdXKtjN1bAGO2bxvahRANCAATwq1k9rx/8neP8MqVR7UuJ98bLFsU5
jpueH0ougZNWrsKUki0cgKDGrb3C8Q2NMRO336ve22Xk674lk/ZDHkAV
-----END PRIVATE KEY-----

ASN.1 格式解析如下:

  • 前面部分是算法標識 1.2.840.10045.2.1(ecPublicKey) 和 1.2.840.10045.3.1.7 (prime256v1)
  • 後面是私鑰信息,其中包括了版本號 1,OCTET STRING 類型私鑰 31B6A1...F6,BIT STRING 類型的公鑰 0000 0100 1111 0000...

當然通過 openssl 可以直接解析出公私鑰和曲線類型,如下:

$ openssl ec -in ecc_prikey.p8 -noout -text
read EC key
Private-Key: (256 bit)
priv:
    31:b6:a1:b1:c2:06:a5:26:7a:35:42:c8:89:0f:e9:
    4c:8f:59:72:f1:5d:5c:ab:63:37:56:c0:18:ed:9b:
    c6:f6
pub:
    04:f0:ab:59:3d:af:1f:fc:9d:e3:fc:32:a5:51:ed:
    4b:89:f7:c6:cb:16:c5:39:8e:9b:9e:1f:4a:2e:81:
    93:56:ae:c2:94:92:2d:1c:80:a0:c6:ad:bd:c2:f1:
    0d:8d:31:13:b7:df:ab:de:db:65:e4:eb:be:25:93:
    f6:43:1e:40:15
ASN1 OID: prime256v1
NIST CURVE: P-256

在上一篇《橢圓曲線密碼學原理分析》一文知道,橢圓曲線的密鑰生成其實就是一個公式 P = nG,n 就是私鑰,G 是基點,P 是公鑰。我們可以通過 libnum 庫來驗證公私鑰的準確性。注意到這裏公鑰的第一個字節 04 表示公鑰格式是 uncompressed format,即非壓縮格式,也就是把點的 X 和 Y 座標合到一起作爲公鑰。壓縮格式就是隻用 X 座標或者 Y 座標中的一個,另一個座標根據曲線方程可以求得([rfc5480] 有詳細說明)。

$ cat ecc.py 
from libnum.ecc import Curve
curve = Curve(
        a=0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc,
        b=0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b,
        p=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff,
        g = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
            0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
)

pri = 0x31b6a1b1c206a5267a3542c8890fe94c8f5972f15d5cab633756c018ed9bc6f6
pub = curve.power(curve.g, pri)
print hex(pub[0]), hex(pub[1])
vagrant@stretch:~$ python ecc.py 
('0xf0ab593daf1ffc9de3fc32a551ed4b89f7c6cb16c5398e9b9e1f4a2e819356aeL', 
'0xc294922d1c80a0c6adbdc2f10d8d3113b7dfabdedb65e4ebbe2593f6431e4015L')

加密私鑰格式

PKCS#8 裏面對私鑰加密提供了 PBES2(Password-Based Encryption Scheme 2)加密模式支持。通過 PBKDF2(Password-Based Key Derivation Function 2) 對原始密碼進行多次哈希處理作爲加密密碼以增強破解難度,然後用對稱加密算法 AES 或者 DES 對私鑰進行加密。

PBKDF2 是一種 CPU 密集型算法,但是如果使用 GPU 陣列或者 FPGA 來破解還是相對容易。在密碼存儲中現在更傾向於用 Bcrypt,它不僅是 CPU 運算密集,而且是內存密集,破解難度會更高一些。不過總的來說, PBKDF2 比 ssh-keygen 的 md5 方式生存密碼安全性會高很多。

PKCS#8 加密類型私鑰的 ASN.1 scheme 定義如下:

EncryptedPrivateKeyInfo ::= SEQUENCE {
     encryptionAlgorithm  EncryptionAlgorithmIdentifier,
     encryptedData        EncryptedData }

     EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
                                        { CONTENT-ENCRYPTION,
                                          { KeyEncryptionAlgorithms } }

     EncryptedData ::= OCTET STRING

使用 openssl 對 PKCS#8 格式的密鑰加密是很方便的,默認就支持(生成密鑰時不加 -nocrypt 參數即可)。加密後的 ecc_prikey.p8 格式解析如下:

  • 其中加密模式是 pkcs5PBES2,密鑰生成算法是 PBKDF2,參數 salt= E20EED9A112B7BFA,iteration=2048,哈希算法是 hmacWithSHA256
  • 對稱加密算法是 aes256-cbc,參數 iv=27579581D081AEDA083889370232AD1A
  • 最後一行 11E9C5C2.... 就是加密私鑰 encryptedData。

加密過程解析:

  • 先使用密鑰生成算法 PBKDF2 生成加密密碼,python 可以用 backports.pbkdf2 模塊。
import os, binascii
from backports.pbkdf2 import pbkdf2_hmac

salt = binascii.unhexlify('E20EED9A112B7BFA')
passwd = b"testtest"
key = pbkdf2_hmac("sha256", passwd, salt, 2048, 32)
print("Derived key:", binascii.hexlify(key))

# 輸出: ('Derived key:', 'bf48084fd98fcbacd8e024166efb7232c897282fe7e4ff836db3f3d81e32ede9')
  • 然後使用 openssl 的 aes256-cbc 加密原私鑰,可以驗證跟 encryptedData 是一樣的。
$ tail -n +2 ecc_prikey.p8 | grep -v 'END '| base64 -d | 
openssl aes-256-cbc -e -iv 27579581D081AEDA083889370232AD1A -K bf48084fd98fcbacd8e024166efb7232c897282fe7e4ff836db3f3d81e32ede9 | hexdump -C

00000000  11 e9 c5 c2 4c c3 2d bb  fa 84 b9 fb db f1 d1 ff  |....L.-.........|
00000010  f0 6a 5b fa c3 a6 88 cd  02 4c ac 52 84 f4 cb c1  |.j[......L.R....|
......
00000080  8f 72 96 7a 58 aa 1f 5a  6f c1 bf dc 43 1a 46 26  |.r.zX..Zo...C.F&|

3.3 PKIX

前面提到 PKCS#8 定義了私鑰格式,可以支持各類私鑰,在 PKIX ([rfc5280]) 中也定義了通用公鑰格式,其中包括算法標識和公鑰內容,算法標識 AlgorithmIdentifier 與前面私鑰中的 PrivateAlgorithmIdentifier 是一樣的。

SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm         AlgorithmIdentifier,
        subjectPublicKey  BIT STRING 
}
     AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  
    }

將之前的 PKCS#1 格式的 RSA 公鑰轉換成 PKIX 的格式:

$ openssl rsa -RSAPublicKey_in -in ../pk1/pubkey.p1 -pubout > pubkey.pkix

PKIX 格式的公鑰解析如下,包括公鑰算法 rsaEncryption 和 RSA 公鑰參數 n 和 e。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章