本文簡要介紹定義 HTTPS 中所使用憑證的資料結構和格式。任何具有少量電腦科學經驗和一些憑證熟悉度的人員都應能理解本文。
HTTPS 憑證是一種檔案類型,就像任何其他檔案一樣。其內容遵循 RFC 5280 所定義的格式。這些定義是以 ASN.1 表示,這是一種用於定義檔案格式或(等效地)資料結構的語言。例如,在 C 語言中,您可以寫成
struct point {
int x, y;
char label[10];
};
在 Go 語言中,您可以寫成
type point struct {
x, y int
label string
}
而在 ASN.1 中,您可以寫成
Point ::= SEQUENCE {
x INTEGER,
y INTEGER,
label UTF8String
}
撰寫 ASN.1 定義而不是 Go 或 C 定義的優點是,它們與語言無關。您可以使用任何語言實作 Point 的 ASN.1 定義,或者(最好)您可以使用一個工具,該工具採用 ASN.1 定義,並自動產生以您喜歡的語言實作該定義的程式碼。一組 ASN.1 定義稱為「模組」。
關於 ASN.1 的另一個重點是,它具有多種序列化格式,這些格式可以將記憶體中的資料結構轉換為一系列位元組(或檔案)並將其還原。這允許一台機器產生的憑證可以被另一台機器讀取,即使該機器使用的是不同的 CPU 和作業系統。
還有一些其他語言的功能與 ASN.1 相同。例如,Protocol Buffers 同時提供用於定義類型的語言和用於編碼已定義類型物件的序列化格式。Thrift 也同時具有語言和序列化格式。Protocol Buffers 或 Thrift 也可以輕鬆地用於定義 HTTPS 憑證的格式,但 ASN.1 (1984) 的優勢在於它在憑證 (1988) 和 HTTPS (1994) 發明時就已經存在。
ASN.1 多年來經過多次修訂,版本通常以發佈年份來識別。本文旨在教導足夠的 ASN.1,以清楚理解 RFC 5280 和其他與 HTTPS 憑證相關的標準,因此我們將主要討論 1988 年的版本,並針對後續版本中新增的功能做一些說明。您可以直接從 ITU 下載各種版本,但請注意,有些版本僅供 ITU 會員使用。相關標準為 X.680 (定義 ASN.1 語言) 和 X.690 (定義序列化格式 DER 和 BER)。這些標準的早期版本分別是 X.208 和 X.209。
ASN.1 的主要序列化格式是「辨別編碼規則」(DER)。它們是新增了正規化的「基本編碼規則」(BER) 的變體。例如,如果類型包含 SET OF,則成員必須針對 DER 序列化進行排序。
以 DER 表示的憑證通常會進一步編碼為 PEM,後者使用 base64 將任意位元組編碼為字母數字字元 (以及 '+' 和 '/'),並新增分隔線 ("-----BEGIN CERTIFICATE-----" 和 “-----END CERTIFICATE-----”)。PEM 非常實用,因為它更容易複製貼上。
本文首先說明 ASN.1 使用的類型和表示法,然後說明如何對使用 ASN.1 定義的物件進行編碼。請隨時在各節之間來回翻閱,特別是當 ASN.1 語言的某些功能直接指定編碼詳細資訊時。本文偏好使用較為熟悉的術語,因此使用「位元組」代替「八位元」,並使用「值」代替「內容」。本文可互換使用「序列化」和「編碼」。
類型
INTEGER
熟悉的 INTEGER。這些可以是正數或負數。ASN.1 INTEGER 真正不尋常的地方在於它們可以任意大。int64 的空間不足?沒問題。這對於表示 RSA 模數之類的東西特別方便,它比 int64 大得多 (例如 22048)。技術上來說,DER 中有一個最大整數,但它非常大:任何 DER 欄位的長度都可以表示為最多 126 個位元組的序列。因此,您可以在 DER 中表示的最大 INTEGER 是 256(2**1008)-1。對於真正無界的 INTEGER,您必須以 BER 進行編碼,這允許無限長的欄位。
字串
ASN.1 有許多字串類型:BMPString、GeneralString、GraphicString、IA5String、ISO646String、NumericString、PrintableString、TeletexString、T61String、UniversalString、UTF8String、VideotexString 和 VisibleString。就 HTTPS 憑證而言,您主要需要關注 PrintableString、UTF8String 和 IA5String。給定欄位的字串類型由定義該欄位的 ASN.1 模組定義。例如
CPSuri ::= IA5String
PrintableString 是 ASCII 的受限子集,允許字母數字、空格和特定少數標點符號:' () + , - . / : = ?
。值得注意的是,它不包括 *
或 @
。使用更多限制性字串類型沒有儲存大小的優勢。
某些欄位 (例如 RFC 5280 中的 DirectoryString) 允許序列化程式碼在多個字串類型之間進行選擇。由於 DER 編碼包括您正在使用的字串類型,因此請確保將某些內容編碼為 PrintableString 時,它確實符合 PrintableString 的要求。
IA5String,基於 國際字母表第 5 號,更具寬容性:它允許幾乎任何 ASCII 字元,並用於憑證中的電子郵件地址、DNS 名稱和 URL。請注意,有一些位元組值,其 IA5 意義與同一值的 US-ASCII 意義不同。
TeletexString、BMPString 和 UniversalString 已棄用,不再用於 HTTPS 憑證,但您在解析較舊的 CA 憑證時可能會看到它們,這些憑證的使用壽命較長,而且可能早於棄用。
ASN.1 中的字串不像 C 和 C++ 中的字串一樣以 Null 結尾。事實上,包含嵌入的 Null 位元組是完全合法的。當兩個系統以不同的方式解釋相同的 ASN.1 字串時,這可能會導致漏洞。例如,一些 CA過去可能會被欺騙而針對「example.com\0.evil.com」發出憑證,原因是在於它擁有 evil.com 的所有權。當時的憑證驗證程式庫將結果視為對「example.com」有效。請非常小心地處理 C 和 C++ 中的 ASN.1 字串,以避免產生漏洞。
日期和時間
同樣地,有多種時間類型:UTCTime、GeneralizedTime、DATE、TIME-OF-DAY、DATE-TIME 和 DURATION。對於 HTTPS 憑證,您只需要關注 UTCTime 和 GeneralizedTime。
UTCTime 將日期和時間表示為 YYMMDDhhmm[ss],並可選擇時區偏移或「Z」表示 Zulu (又稱為 UTC,又稱為 0 時區偏移)。例如,UTCTimes 820102120000Z 和 820102070000-0500 都表示相同的時間:1982 年 1 月 2 日紐約市 (UTC-5) 上午 7 點和 UTC 下午 12 點。
由於 UTCTime 對於 1900 年代或 2000 年代是否含糊不清,RFC 5280 闡明它表示 1950 年到 2050 年的日期。RFC 5280 也要求必須使用「Z」時區,而且必須包含秒數。
GeneralizedTime 透過使用四位數字表示年份,支援 2050 年之後的日期。它也允許小數秒 (奇怪的是,小數分隔符號可以使用逗號或句號)。RFC 5280 禁止使用小數秒,並要求使用「Z」。
OBJECT IDENTIFIER
物件識別符號是由一系列整數組成的全域唯一、階層式識別符號。它們可以參照任何類型的「事物」,但通常用於識別標準、演算法、憑證擴充功能、組織或政策文件。例如:1.2.840.113549 會識別 RSA Security LLC。然後,RSA 可以指定以此前綴開頭的 OID,例如 1.2.840.113549.1.1.11,它會識別 RFC 8017 中定義的 sha256WithRSAEncryption。
同樣地,1.3.6.1.4.1.11129 會識別 Google, Inc.。Google 將 1.3.6.1.4.1.11129.2.4.2 指定為識別憑證透明度中使用的 SCT 清單擴充功能 (最初在 Google 開發),如 RFC 6962 中所定義。
在給定前綴下可能存在的子 OID 集稱為「OID 弧」。由於較短 OID 的表示較小,因此較短弧下的 OID 指定被認為更有價值,特別是對於必須傳送大量 OID 的格式。OID 弧 2.5 會指定給「目錄服務」,這是包含 X.509 的一組規格,而 HTTPS 憑證即基於此規格。憑證中的許多欄位都以這個方便的短弧開頭。例如,2.5.4.6 表示「countryName」,而 2.5.4.10 表示「organizationName」。由於大多數憑證都必須至少編碼每個 OID 一次,因此它們很短很方便。
為了方便起見,規格中的 OID 通常使用人類可讀的名稱表示,並且可以透過與另一個 OID 連接來指定。例如,來自 RFC 8017
pkcs-1 OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
}
...
sha256WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 11 }
NULL
NULL 就是 NULL,你知道的吧?
SEQUENCE 和 SEQUENCE OF
不要讓名稱誤導您:這是兩種非常不同的類型。SEQUENCE 相當於大多數程式語言中的「struct」。它包含固定數量的不同類型欄位。例如,請參閱下方的憑證範例。
另一方面,SEQUENCE OF 包含任意數量的單一類型欄位。這類似於程式語言中的陣列或清單。例如
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
這可以是 0 個、1 個或 7,000 個依特定順序的 RelativeDistinguishedName。
事實證明,SEQUENCE 和 SEQUENCE OF 確實有一個相似之處,那就是它們都以相同的方式編碼!有關更多資訊,請參閱編碼一節。
SET 和 SET OF
它們與 SEQUENCE 和 SEQUENCE OF 非常相似,差別在於它們的元素順序沒有任何語義上的意義。然而,在編碼形式中,它們必須經過排序。範例
RelativeDistinguishedName ::=
SET SIZE (1..MAX) OF AttributeTypeAndValue
注意:此範例額外使用 SIZE 關鍵字來指定 RelativeDistinguishedName 至少要有一個成員,但一般來說,SET 或 SET OF 允許大小為零。
BIT STRING 和 OCTET STRING
它們分別包含任意的位元或位元組。它們可以用來保存非結構化數據,如 nonce 或雜湊函數的輸出。它們也可以像 C 語言中的 void 指標或 Go 語言中的空介面類型 (interface{}) 一樣使用:一種保存具有結構的數據的方式,但該結構是獨立於類型系統來理解或定義的。例如,憑證上的簽章被定義為 BIT STRING。
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING }
較新版本的 ASN.1 語言允許更詳細地指定 BIT STRING 內的內容(OCTET STRING 也適用)。
CHOICE 和 ANY
CHOICE 是一種可以包含其定義中所列出的類型之一的類型。例如,Time 可以只包含 UTCTime 或 GeneralizedTime 其中之一。
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime }
ANY 表示值可以是任何類型。實際上,它通常會受到 ASN.1 語法無法完全表達的限制。例如
AttributeTypeAndValue ::= SEQUENCE {
type AttributeType,
value AttributeValue }
AttributeType ::= OBJECT IDENTIFIER
AttributeValue ::= ANY -- DEFINED BY AttributeType
這對於擴充功能特別有用,您希望在主要規格發佈後,可以保留額外欄位以供單獨定義,這樣您就有方法註冊新類型(物件識別碼),並允許這些類型的定義指定新欄位的結構。
請注意,ANY 是 1988 年 ASN.1 標記法的遺留物。在1994 年版本中,ANY 已被棄用,並以資訊物件類別取代,這是一種正式化的方式,可指定人們希望從 ANY 獲得的擴充行為。這個變更到現在已經很老舊了,以至於最新的 ASN.1 規格(自 2015 年起)甚至沒有提到 ANY。但是,如果您查看 1994 年版本,您可以看到一些關於轉換的討論。我在此處包含較舊的語法,因為 RFC 5280 仍然使用它。RFC 5912 使用 2002 年的 ASN.1 語法來表達 RFC 5280 和幾個相關規格中的相同類型。
其他標記法
註解以 --
開頭。SEQUENCE 或 SET 的欄位可以標記為 OPTIONAL,或者可以標記為 DEFAULT foo,這與 OPTIONAL 的意義相同,只是當該欄位不存在時,應視為包含「foo」。具有長度的類型(字串、八位元組和位元字串、集合和序列 OF 物件)可以給定一個 SIZE 參數,以限制其長度,可以是確切的長度或長度範圍。
可以使用類型定義後的花括號來限制類型必須具有特定值。此範例定義 Version 欄位可以有三個值,並為這些值指定有意義的名稱。
Version ::= INTEGER { v1(0), v2(1), v3(2) }
這也常用於為特定的 OID 指定名稱(請注意,這是一個單一值,沒有逗號表示替代值)。來自 RFC 5280 的範例。
id-pkix OBJECT IDENTIFIER ::=
{ iso(1) identified-organization(3) dod(6) internet(1)
security(5) mechanisms(5) pkix(7) }
您還會看到 [number]、IMPLICIT、EXPLICIT、UNIVERSAL 和 APPLICATION。這些定義了值應該如何編碼的細節,我們將在下面討論。
編碼
ASN.1 與許多編碼相關聯:BER、DER、PER、XER 等。基本編碼規則 (BER) 相當靈活。獨特編碼規則 (DER) 是 BER 的子集,具有正規化規則,因此只有一種方法可以表達給定的結構。封包編碼規則 (PER) 使用較少的位元組來編碼事物,因此當空間或傳輸時間很寶貴時,它們會很有用。當您因故想要使用 XML 時,XML 編碼規則 (XER) 會很有用。
HTTPS 憑證通常以 DER 編碼。可以將它們以 BER 編碼,但是由於簽章值是根據等效的 DER 編碼計算的,而不是憑證中的確切位元組,因此以 BER 編碼憑證會導致不必要的麻煩。我將描述 BER,並在過程中解釋 DER 提供的額外限制。
我建議您在閱讀本節時,同時開啟另一個視窗,查看這個真實憑證的解碼。
類型-長度-值
BER 是一種類型-長度-值編碼,就像 Protocol Buffers 和 Thrift 一樣。這意味著,當您讀取以 BER 編碼的位元組時,首先會遇到一個稱為標籤的類型 (tag),在 ASN.1 中。這是一個位元組或一系列位元組,告訴您編碼的事物類型:INTEGER、UTF8String、結構或其他任何東西。
類型 | 長度 | 值 |
---|---|---|
02 | 03 | 01 00 01 |
接下來,您會遇到長度:一個數字,告訴您需要讀取多少位元組的數據才能取得值。然後,當然,就是包含值本身的位元組。例如,十六進位位元組 02 03 01 00 01 將表示一個 INTEGER(標籤 02 對應於 INTEGER 類型),長度為 03,以及一個由 01 00 01 組成的三位元組值。
類型-長度-值與像 JSON、CSV 或 XML 這種分隔符號編碼不同,在分隔符號編碼中,您不是預先知道欄位的長度,而是讀取位元組直到遇到預期的分隔符號(例如 JSON 中的 }
或 XML 中的 </some-tag>
)。
標籤
標籤通常是一個位元組。有一種使用多個位元組(「高標籤號」形式)來編碼任意大的標籤號的方法,但通常不需要這樣做。
以下是一些標籤範例
標籤(十進位) | 標籤(十六進位) | 類型 |
---|---|---|
2 | 02 | INTEGER |
3 | 03 | BIT STRING |
4 | 04 | OCTET STRING |
5 | 05 | NULL |
6 | 06 | OBJECT IDENTIFIER |
12 | 0C | UTF8String |
16 | 10(和 30)* | SEQUENCE 和 SEQUENCE OF |
17 | 11(和 31)* | SET 和 SET OF |
19 | 13 | PrintableString |
22 | 16 | IA5String |
23 | 17 | UTCTime |
24 | 18 | GeneralizedTime |
這些以及我為了避免無趣而跳過的其他一些標籤是「通用」標籤,因為它們在核心 ASN.1 規範中指定,並且在所有 ASN.1 模組中都表示相同的意義。
這些標籤都碰巧小於 31 (0x1F),這是有原因的:位元 8、7 和 6(標籤位元組的高位元)用於編碼額外資訊,因此任何大於 31 的通用標籤號都需要使用「高標籤號」形式,這需要額外的位元組。有少數通用標籤大於 31,但它們非常罕見。
標記有 *
的兩個標籤始終編碼為 0x30 或 0x31,因為位元 6 用於指示欄位是建構式還是基本式。這些標籤始終是建構式的,因此它們的編碼將位元 6 設定為 1。有關詳細資訊,請參閱建構式與基本式章節。
標籤類別
僅僅因為通用類別已經用完了所有「良好」的標籤號,並不表示我們無法定義自己的標籤。還有「應用程式」、「私人」和「內容特定」類別。這些類別透過位元 8 和 7 來區分。
類別 | 位元 8 | 位元 7 |
---|---|---|
通用 | 0 | 0 |
應用程式 | 0 | 1 |
內容特定 | 1 | 0 |
私人 | 1 | 1 |
規格大多使用通用類別中的標籤,因為它們提供了最重要的建構區塊。例如,憑證中的序號以普通的 INTEGER 編碼,標籤號為 0x02。但有時規格需要在內容特定類別中定義標籤,以消除 SET 或 SEQUENCE 中定義可選條目的歧義,或消除具有相同類型的多個條目的 CHOICE 的歧義。例如,採用此定義
Point ::= SEQUENCE {
x INTEGER OPTIONAL,
y INTEGER OPTIONAL
}
由於 OPTIONAL 欄位在不存在時會完全從編碼中省略,因此不可能區分只有 x 坐標的 Point 與只有 y 坐標的 Point。例如,您將只包含 x 坐標為 9 的 Point 編碼如下(30 在這裡表示 SEQUENCE)
30 03 02 01 09
這是一個長度為 3(位元組)的 SEQUENCE,包含一個長度為 1 的 INTEGER,其值為 9。但是,您也會以完全相同的方式編碼 y 坐標為 9 的 Point,因此會產生歧義。
編碼指示
為了消除這種歧義,規格需要提供編碼指示,為每個條目指定唯一的標籤。而且由於我們不允許踩踏 UNIVERSAL 標籤,因此我們必須使用其他標籤之一,例如 APPLICATION
Point ::= SEQUENCE {
x [APPLICATION 0] INTEGER OPTIONAL,
y [APPLICATION 1] INTEGER OPTIONAL
}
雖然對於這種用例,實際上更常見的是使用內容特定類別,它本身以括號中的數字表示
Point ::= SEQUENCE {
x [0] INTEGER OPTIONAL,
y [1] INTEGER OPTIONAL
}
因此,現在,若要編碼只有 x 坐標為 9 的 Point,您不是將 x 編碼為 UNIVERSAL INTEGER,而是將編碼標籤的位元 8 和 7 設定為 (1, 0) 以指示內容特定類別,並將低位元設定為 0,產生此編碼
30 03 80 01 09
而要表示只有 y 坐標為 9 的 Point,您會執行相同的操作,只是會將低位元設定為 1
30 03 81 01 09
或者您可以表示 x 和 y 坐標都等於 9 的 Point
30 06 80 01 09 81 01 09
長度
標籤-長度-值元組中的長度始終表示物件中的位元組總數,包括所有子物件。因此,只有一個欄位的 SEQUENCE 沒有長度 1;它的長度為該欄位的編碼形式所佔用的位元組數。
長度的編碼可以採用兩種形式:短形式或長形式。短形式是一個位元組,介於 0 和 127 之間。
長形式至少為兩個位元組長,並且第一個位元組的位元 8 設定為 1。第一個位元組的位元 7-1 指示長度欄位本身還有多少個位元組。然後,其餘位元組以多位元組整數的形式指定長度本身。
如您所想,這允許非常長的值。最長的可能長度將以位元組 254 開始(長度位元組 255 保留給未來擴展),指定長度欄位本身將接著 126 個位元組。如果這 126 個位元組中的每一個都是 255,則表示值欄位中將接著 21008-1 個位元組。
長格式允許您用多種方式編碼相同的長度 - 例如,使用兩個位元組來表示一個可以用一個位元組容納的長度,或者使用長格式來表示一個可以用短格式容納的長度。DER 規定始終使用最小的可能長度表示法。
安全警告:不要完全信任您解碼的長度值!例如,檢查編碼的長度是否小於要解碼的串流中可用的資料量。
不定長度
在 BER 中,也可以編碼一個字串、SEQUENCE、SEQUENCE OF、SET 或 SET OF,其中您事先不知道長度(例如,在串流輸出時)。為此,您將長度編碼為值為 80 的單個位元組,並將該值編碼為一系列串聯在一起的編碼物件,結尾由兩個位元組 00 00
表示(可以將其視為標籤為 0 的零長度物件)。因此,例如,UTF8String 的不定長度編碼將是一個或多個串聯在一起的 UTF8String 的編碼,最後與 00 00 串聯。
不確定性可以任意嵌套!因此,舉例來說,您串聯在一起形成不定長度 UTF8String 的 UTF8String 本身可以使用確定長度或不定長度進行編碼。
長度位元組 80 很特別,因為它不是有效的短格式或長格式長度。由於第 8 位設定為 1,因此通常會將其解釋為長格式,但其餘位元應指示構成長度的其他位元組數。由於第 7-1 位均為 0,因此表示使用零位元組構成長度的長格式編碼,這是不能允許的。
DER 禁止不定長度編碼。您必須使用確定長度編碼(也就是說,長度在開頭指定)。
構造式 vs 原始式
第一個標籤位元組的第 6 位用於指示值是以原始形式還是構造形式編碼。原始編碼直接表示該值 - 例如,在 UTF8String 中,該值僅由字串本身組成(以 UTF-8 位元組表示)。構造編碼將該值表示為其他編碼值的串聯。例如,如「不定長度」部分所述,構造編碼中的 UTF8String 將由多個編碼的 UTF8String(每個都有標籤和長度)串聯在一起組成。整體 UTF8String 的長度將是所有那些串聯的編碼值的總長度(以位元組為單位)。構造編碼可以使用確定長度或不定長度。原始編碼始終使用確定長度,因為沒有辦法在不使用構造編碼的情況下表示不定長度。
INTEGER、OBJECT IDENTIFIER 和 NULL 必須使用原始編碼。SEQUENCE、SEQUENCE OF、SET 和 SET OF 必須使用構造編碼(因為它們本質上是多個值的串聯)。BIT STRING、OCTET STRING、UTCTime、GeneralizedTime 和各種字串類型可以使用原始編碼或構造編碼,由發送者自行決定 - 在 BER 中。但是,在 DER 中,所有在原始編碼和構造編碼之間具有編碼選擇的類型都必須使用原始編碼。
EXPLICIT vs IMPLICIT
上面描述的編碼說明,例如 [1]
或 [APPLICATION 8]
,也可以包含關鍵字 EXPLICIT 或 IMPLICIT(RFC 5280 中的範例)。
TBSCertificate ::= SEQUENCE {
version [0] Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] Extensions OPTIONAL
-- If present, version MUST be v3 -- }
這定義了應如何編碼標籤;它與是否明確分配標籤號無關(因為 IMPLICIT 和 EXPLICIT 始終與特定的標籤號一起使用)。IMPLICIT 像基礎類型一樣編碼欄位,但具有 ASN.1 模組中提供的標籤號和類別。EXPLICIT 將該欄位編碼為基礎類型,然後將其包裝在外部編碼中。外部編碼具有 ASN.1 模組中的標籤號和類別,並且還設定了構造位。
以下是使用 IMPLICIT 的 ASN.1 編碼指令範例
[5] IMPLICIT UTF8String
這會將 "hi" 編碼為
85 02 68 69
與此使用 EXPLICIT 的 ASN.1 編碼指令進行比較
[5] EXPLICIT UTF8String
這會將 "hi" 編碼為
A5 04 0C 02 68 69
當不存在 IMPLICIT 或 EXPLICIT 關鍵字時,預設值為 EXPLICIT,除非模組在頂部使用「EXPLICIT TAGS」、「IMPLICIT TAGS」或「AUTOMATIC TAGS」設定不同的預設值。例如,RFC 5280 定義了兩個模組,一個模組中EXPLICIT 標籤為預設值,另一個模組導入第一個模組,並且IMPLICIT 標籤為預設值。隱式編碼使用的位元組比顯式編碼少。
AUTOMATIC TAGS 與 IMPLICIT TAGS 相同,但具有額外的屬性,即在需要它們的位置(例如具有可選欄位的 SEQUENCE)自動分配標籤號([0]
、[1]
等)。
特定類型的編碼
在本節中,我們將討論如何編碼每個類型的值,並提供範例。
INTEGER 編碼
整數編碼為一個或多個位元組,以二補數表示,最左側位元組的高位(第 8 位)作為符號位。正如 BER 規格所說
二補數二進位數的值是透過對內容八位元組中的位進行編號導出的,從最後一個八位元組的第 1 位作為零位開始,到第一個八位元組的第 8 位結束編號。每個位都被賦予 2N 的數值,其中 N 是它在上述編號序列中的位置。二補數二進位數的值是透過對設定為 1 的每個位的數值求和獲得的,不包括第一個八位元組的第 8 位,然後如果該位設定為 1,則將該值減去第一個八位元組的第 8 位的數值。
例如,此單位元組值(以二進位表示)編碼十進位 50
00110010(== 十進位 50)
此單位元組值(以二進位表示)編碼十進位 -100
10011100(== 十進位 -100)
此五個位元組的值(以二進位表示)編碼十進位 -549755813887(即 -239 + 1)
10000000 00000000 00000000 00000000 00000001(== 十進位 -549755813887)
BER 和 DER 都要求整數以盡可能短的形式表示。這是透過此規則強制執行的
... the bits of the first octet and bit 8 of the second octet:
1. shall not all be ones; and
2. shall not all be zero.
規則 (2) 大致意思是:如果在編碼中有前導零位元組,您可以省略它們並具有相同的數字。第二個位元組的第 8 位在這裡也很重要,因為如果您要表示某些值,則必須使用前導零位元組。例如,十進位 255 編碼為兩個位元組
00000000 11111111
那是因為單位元組編碼 11111111 本身表示 -1(第 8 位被視為符號位)。
規則 (1) 最好用一個範例解釋。十進位 -128 編碼為
10000000(== 十進位 -128)
但是,也可以將其編碼為
11111111 10000000(== 十進位 -128,但無效的編碼)
展開來看,它是 -215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 == -27 == -128。請注意,「10000000」中的 1 在單位元組編碼中是符號位,但在雙位元組編碼中表示 27。
這是一個通用的轉換:對於任何編碼為 BER(或 DER)的負數,您可以在其前面加上 11111111 並獲得相同的數字。這稱為符號擴展。或者等效地,如果有一個負數,其值的編碼以 11111111 開頭,則可以移除該位元組並仍然具有相同的數字。因此,BER 和 DER 要求最短的編碼。
INTEGER 的二補數編碼在憑證頒發中具有實際影響:RFC 5280 要求序號必須為正數。由於第一位始終是符號位,這表示以 DER 編碼為 8 個位元組的序號最多可以為 63 位元長。編碼 64 位元正序號需要 9 位元組的編碼值(第一個位元組為零)。
以下是以值 263+1(恰好是 64 位元正數)編碼 INTEGER 的範例
02 09 00 80 00 00 00 00 00 00 01
字串編碼
字串以其文字位元組編碼。由於 IA5String 和 PrintableString 僅定義了可接受字元的不同子集,因此它們的編碼僅在標籤上有所不同。
包含 "hi" 的 PrintableString
13 02 68 69
包含 "hi" 的 IA5String
16 02 68 69
UTF8String 相同,但可以編碼更多種類的字元。例如,這是包含 U+1F60E 微笑太陽眼鏡表情符號 (😎) 的 UTF8String 的編碼
0c 04 f0 9f 98 8e
日期和時間編碼
令人驚訝的是,UTCTime 和 GeneralizedTime 實際上像字串一樣編碼!如上文「類型」部分所述,UTCTime 以 YYMMDDhhmmss 格式表示日期。GeneralizedTime 使用四位數年份 YYYY 代替 YY。兩者都有一個可選時區偏移量或 “Z”(Zulu)來表示與 UTC 沒有時區偏移量。
例如,在 PST 時區 (UTC-8) 中 2019 年 12 月 15 日 19:02:10 在 UTCTime 中表示為:191215190210-0800。在 BER 中編碼,為
17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30
對於 BER 編碼,在 UTCTime 和 GeneralizedTime 中,秒數都是可選的,並且允許時區偏移量。但是,DER(連同 RFC 5280)指定必須存在秒數,不得存在小數秒,並且時間必須以 UTC 表示,形式為 “Z”。
上述日期在 DER 中將編碼為
17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a
OBJECT IDENTIFIER 編碼
如上文所述,OID 在概念上是一系列整數。它們始終至少有兩個元件長。第一個元件始終為 0、1 或 2。當第一個元件為 0 或 1 時,第二個元件始終小於 40。因此,前兩個元件明確地表示為 40*X+Y,其中 X 是第一個元件,Y 是第二個元件。
因此,例如,要編碼 2.999.3,您會將前兩個元件組合為 1079 十進位 (40*2 + 999),這會給您 “1079.3”。
應用該轉換後,每個元件都以 128 為基底編碼,最重要的位元組在前。除了元件中的最後一個位元組之外,每個位元組的第 8 位都設定為 “1”;這就是您知道一個元件何時完成且下一個元件開始的方式。因此,元件 “3” 將簡單地表示為位元組 0x03。元件 “129” 將表示為位元組 0x81 0x01。編碼後,OID 的所有元件會串聯在一起,以形成 OID 的編碼值。
無論是在 BER 還是 DER 中,OID 都必須以最少的位元組表示。因此元件不能以位元組 0x80 開頭。
例如,OID 1.2.840.113549.1.1.11(表示sha256WithRSAEncryption)的編碼如下
06 09 2a 86 48 86 f7 0d 01 01 0b
NULL 編碼
包含 NULL 的物件的值始終為零長度,因此 NULL 的編碼始終只是標籤和長度欄位為零
05 00
SEQUENCE 編碼
關於 SEQUENCE 首先要知道的是,它始終使用構造編碼,因為它包含其他物件。換句話說,SEQUENCE 的值位元組包含該 SEQUENCE 的編碼欄位的串聯(按照這些欄位定義的順序)。這也表示 SEQUENCE 標籤的第 6 位(構造式 vs 原始式 位)始終設定為 1。因此,即使 SEQUENCE 的標籤號在技術上為 0x10,一旦編碼,其標籤位元組始終為 0x30。
當 SEQUENCE 中有帶有 OPTIONAL 註解的欄位時,如果不存在,則會將它們從編碼中省略。當解碼器處理 SEQUENCE 的元素時,它可以根據到目前為止已解碼的內容以及它讀取的標籤位元組來判斷正在解碼的類型。如果有歧義,例如當元素具有相同的類型時,ASN.1 模組必須指定編碼指令,為元素分配不同的標籤號。
DEFAULT 欄位與 OPTIONAL 欄位類似。如果欄位的值是預設值,則在 BER 編碼中可以省略。在 DER 編碼中,則必須省略。
例如,RFC 5280 將 AlgorithmIdentifier 定義為 SEQUENCE
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
以下是包含 1.2.840.113549.1.1.11 的 AlgorithmIdentifier 的編碼。RFC 8017 說明 「參數」對於此演算法應具有 NULL 類型。
30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00
SEQUENCE OF 編碼
SEQUENCE OF 的編碼方式與 SEQUENCE 完全相同。它甚至使用相同的標籤!如果您正在解碼,唯一可以區分 SEQUENCE 和 SEQUENCE OF 的方法是參考 ASN.1 模組。
以下是包含數字 7、8 和 9 的 INTEGER 序列的編碼
30 09 02 01 07 02 01 08 02 01 09
SET 編碼
與 SEQUENCE 類似,SET 是結構化的,這意味著其值位元組是其編碼欄位的串聯。其標籤編號為 0x11。由於結構化與基本位元(第 6 位元)始終設為 1,這表示它使用 0x31 的標籤位元組進行編碼。
SET 的編碼,與 SEQUENCE 類似,如果 OPTIONAL 和 DEFAULT 欄位不存在或具有預設值,則會省略。任何因具有相同類型的欄位而導致的歧義都必須由 ASN.1 模組解決,並且如果 DEFAULT 欄位具有預設值,則必須從 DER 編碼中省略。
在 BER 中,SET 可以以任何順序編碼。在 DER 中,SET 必須按照每個元素的序列化值升序編碼。
SET OF 編碼
SET OF 項目以與 SET 相同的方式編碼,包括 0x31 的標籤位元組。對於 DER 編碼,有類似的要求,SET OF 必須按照升序編碼。由於 SET OF 中的所有元素都具有相同的類型,因此按標籤排序是不夠的。因此,SET OF 的元素會按照其編碼值排序,較短的值會被視為在右側填充零。
BIT STRING 編碼
N 位元的 BIT STRING 編碼為 N/8 個位元組(向上取整),並帶有一個位元組的前綴,其中包含「未使用的位元數」,以便在位元數不是 8 的倍數時清楚表示。例如,當編碼位元字串 011011100101110111(18 位元)時,我們至少需要三個位元組。但是這比我們需要的還要多一些:它總共提供了 24 位元的容量。其中六個位元將未使用。這六個位元寫在位元字串的最右側,因此編碼為
03 04 06 6e 5d c0
在 BER 中,未使用的位元可以有任何值,因此該編碼的最後一個位元組也可以是 c1、c2、c3 等等。在 DER 中,未使用的位元必須全部為零。
OCTET STRING 編碼
OCTET STRING 編碼為其包含的位元組。以下是包含位元組 03、02、06 和 A0 的 OCTET STRING 的範例
04 04 03 02 06 A0
CHOICE 和 ANY 編碼
CHOICE 或 ANY 欄位會編碼為它實際包含的任何類型,除非被編碼指示修改。因此,如果 ASN.1 規格中的 CHOICE 欄位允許 INTEGER 或 UTCTime,並且正在編碼的特定物件包含 INTEGER,則會將其編碼為 INTEGER。
實際上,CHOICE 欄位通常有編碼指示。例如,請考慮 RFC 5280 中的這個範例,其中編碼指示對於區分 rfc822Name 和 dNSName 是必要的,因為它們都具有底層類型 IA5String
GeneralName ::= CHOICE {
otherName [0] OtherName,
rfc822Name [1] IA5String,
dNSName [2] IA5String,
x400Address [3] ORAddress,
directoryName [4] Name,
ediPartyName [5] EDIPartyName,
uniformResourceIdentifier [6] IA5String,
iPAddress [7] OCTET STRING,
registeredID [8] OBJECT IDENTIFIER }
以下是包含 rfc822Name a@example.com
的 GeneralName 的範例編碼(回想一下,[1] 表示使用標籤編號 1,在標籤類別「context-specific」(第 8 位元設為 1)中,使用 IMPLICIT 標籤編碼方法)
81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d
以下是包含 dNSName "example.com" 的 GeneralName 的範例編碼
82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d
安全性
在解碼 BER 和 DER 時,務必非常小心,尤其是在 C 和 C++ 等非記憶體安全的語言中。解碼器存在很長一段時間的漏洞歷史。一般來說,解析輸入是常見的漏洞來源。特別是 ASN.1 編碼格式似乎是特別容易出現漏洞的磁鐵。它們是複雜的格式,具有許多可變長度的欄位。甚至長度也具有可變長度!此外,ASN.1 輸入通常由攻擊者控制。如果您必須解析憑證才能區分授權使用者和未授權使用者,您必須假設有時您將解析的不是憑證,而是一些旨在利用您的 ASN.1 程式碼中錯誤的奇怪輸入。
為了避免這些問題,最好盡可能使用記憶體安全的語言。無論您是否可以使用記憶體安全的語言,最好使用ASN.1 編譯器來產生您的解析程式碼,而不是從頭開始編寫。
致謝
我非常感謝ASN.1、DER 和 BER 子集的入門指南,這是我學習這些主題的重要一部分。我也要感謝熱烈歡迎 DNS 的作者,這是一本很棒的讀物,並啟發了本文的語氣。
一點額外資訊
您是否曾注意過 PEM 編碼的憑證總是從「MII」開始?例如
-----BEGIN CERTIFICATE-----
MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
...
現在您了解足夠多的資訊來解釋原因! 憑證是一個 SEQUENCE,因此它將以位元組 0x30 開頭。下一個位元組是長度欄位。憑證幾乎總是超過 127 個位元組,因此長度欄位必須使用長格式的長度。這表示第一個位元組將是 0x80 + N,其中 N 是要跟隨的長度位元組的數量。N 幾乎總是 2,因為這就是編碼 128 到 65535 長度所需的位元組數,而且幾乎所有憑證的長度都在該範圍內。
因此,我們現在知道憑證的 DER 編碼的前兩個位元組是 0x30 0x82。PEM 編碼使用base64,它將 3 個位元組的二進位輸入編碼為 4 個 ASCII 字元的輸出。或者,換句話說:base64 將 24 位元的二進位輸入轉換為 4 個 ASCII 字元的輸出,每個字元分配 6 位元的輸入。我們知道每個憑證的前 16 位元會是什麼。為了證明(幾乎)每個憑證的前幾個字元都是「MII」,我們需要看一下接下來的 2 位元。它們將是兩個長度位元組中最重要的位元組的最高有效位元。這些位元會被設定為 1 嗎?除非憑證的長度超過 16,383 個位元組,否則不會!因此,我們可以預測 PEM 憑證的前幾個字元將始終相同。自己試試看
xxd -r -p <<<308200 | base64