深入剖析 RSA 密鑰原理及實踐

一、前言

在經歷了人生的很多至暗時刻后,你讀到了這篇文章,你會后悔甚至憤怒:為什么你沒有早點寫出這篇文章?!

你的至暗時刻包括:

1.你所在的項目需要對接銀行,對方需要你提供一個加密證書。你手上只有一個六級英語證書,不確定這個是否滿足對方需求。由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。

2. 你老驥伏櫪 2 個月,終于搞懂了.crt 格式證書。加入到新項目,項目在進行證書托管改造。哈哈,這題我會,就是把證書文件上傳到托管系統。你對項目組成員大喝一聲,放開那些證書,讓我來!擠進去一看,是陳年老項目了,根本沒有證書,當時使用是公鑰和私鑰,如何公鑰和私鑰變成證書??由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。

3. 你臥薪嘗膽 3 個月,摸清楚了 SSL 證書的來龍去脈。躊躇滿志加入到新項目,你向項目經理痛陳血淚史,經此一役,你已經成長為安全證書方面的專家。項目經理喜出望外,正好項目在進行數據安全改造,數據庫需要啟用 SSL,來得正是時候,不著急,明天下班前提供幾個密鑰文件就行。越明日,下班前半小時,你緩緩走向項目經理,“你要的貨到了”,便排出三個證書,這個是 key 文件,這個是公鑰文件,這個是證書文件。項目經理點點頭又搖搖頭,我要的是JKS 文件呀。你說,明天提供。越明日,下班前的半個小時,你把 JKS 格式文件交給項目經理,項目經理點點頭又搖搖頭,密碼呢?沒有密碼怎么行?由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。

本文將從以下幾部分來揭示 RSA 密鑰文件的鮮為人知的秘密:

  • RSA 算法數學基礎
  • RSA秘鑰體系六層模型
  • RSA 工具使用
  • RSA密鑰使用場景

注:雖然密鑰與證書嚴格意義上并不等同,但為了表述方便,沒有特殊指定的話,本文中的密鑰一詞涵蓋了公鑰,私鑰,證書等概念。

二、RSA 算法數學基礎

RSA 算法是基于數論的,RSA算法的復雜性的基礎在于一個大數的素數分解是NP難題,非常難破解。RSA 算法相關的數學概念:

image

對于任意一個數 x,可以計算出 y:

[圖片上傳失敗...(image-b9fe3d-1611021868265)]

通過 y,可以計算出 x:

[圖片上傳失敗...(image-6b9a6f-1611021868265)]

也就是說,x 通過數對 (m,e) 生成了 y 后,可以通過數對 (m, d) 將 y 還原成 x。

這里,我們實際上演示了RSA加密解密的數學過程。通過公式 (1),根據 x 計算得出 y 的過程就是加密,通過公式 (2),根據 y 計算得出 x 的過程就是解密。

在實際應用中,RSA 算法通過公鑰進行加密,私鑰進行解密,因此數對 (m,e) 就是公鑰,(m, d) 就是私鑰。

實際上為了提高私鑰解密速度,私鑰會保存一些中間結果,例如 p, q, e, 等等。

所以在實際應用中,可以通過私鑰導出公鑰。

三、 RSA秘鑰六層模型

為了方便理解RSA密鑰的原理,本人創造性地發明了RSA密鑰六層模型概念。每一層定義了自己的職責和邊界,層級越低,其表示的內容越傾向于抽象和理論;層級越高,其表示的內容越傾向于實際應用。

image
  • Data:數據層,定義了RSA密鑰的數學概念(m,e,p,q等)或者參與實體(subject, issuer等)。
  • Serialization:序列化層,定義了將復雜數據結構序列化的方法。
  • Structure:結構層,定義了不同格式的RSA密鑰的數據組織形式。
  • Text:文本層,定義了將二進制的密鑰轉換成文本的方法。
  • Presentation:表現層,定義了文本格式密鑰的表現形式。
  • Application:應用層,定義了RSA密鑰使用的各種場景。

下面對每一層進行具體說明。

3.2 數據層

從上文可知,秘鑰是一個數據結構,每個結構包含了 2 個或更多的成員(公鑰包含 m 和 e,私鑰包含 m,d,e 以及其他一些中間結果)。為了將這些數據結構保存在文件中,需要定義某種格式對秘鑰進行序列化。

3.3 序列化層

目前常見的定義數據結構的格式包括 JSON 和 XML 等文本格式。

比如,理論上我們可以把公鑰定義為一個 JSON:

JSON格式密鑰

{
    "m":"15",
    "e":"3"
}

或者,也可以把私鑰定義為一個 XML:

<?xml>
<key>
    <module>15</module>
    <e>3</e>
    <d>3</d>
    <p>3</p>
    <q>5</q>
<key>

但是 RSA 發明的時候,這兩種格式都還不存在。因此科學家們選擇了當時比較流行的語法格式ASN.1。

3.3.1 ASN.1

ASN.1 全稱是 Abstract Syntax Notation dot one,(抽象語法記號第1版)。數字1被ISO加在ASN的后邊,是為了保持ASN的開放性,可以讓以后功能更加強大的ASN被命名為ASN.2等,但至今也沒有出現。

ASN.1描述了一種對數據進行表示、編碼、傳輸和解碼的數據格式。它提供了一整套正規的格式用于描述對象的結構,而不管語言上如何執行及這些數據的具體指代,也不用去管到底是什么樣的應用程序。

3.3.2 ASN.1 編碼規則

ASN.1的具體語法可以參考維基百科(https://zh.wikipedia.org/wiki/ASN.1),在此只作簡要說明。

ASN.1 中數據類型表示是 T-L-V 的形式:頭 2 個字節代表數據類型,接下來的 2 個字節代表字節長度,V 代表具體值。常見的基礎類型的值包括 Integer, UTF8String, 復合結構包括 SEQUENCE, SET.秘鑰和證書都是 SEQUENCE 類型,而 SEQUENCE 的 type 是 0x30,且長度是大于 127 的,因此第2 個字節是 0x82. ASN.1 編碼表示的數據是二進制數據,通常通過 BASE64 轉化成字符串保存在 pem 文件中,而 0x3082 經過 BASE64 編碼后,就是字符串 MI,因此所有 PEM 文件存儲的秘鑰開始的前兩個字符是 MI。

BER, CER, DER 是 ASN.1 編碼規則。其中 DER(Distinguish Encode Rules) 是無歧義編碼規則,保證相同的數據結構產生的序列化結果也相同的。

ASN.1 只是定義了抽象數據的序列化方式,但是具體的編碼還需要進一步定義。

嚴格來說,ASN.1 還不是一種定義數據的格式,而是一種語法標準,按照這種標準,可以制定各種各樣的格式。

3.4 結構層

根據秘鑰文件用途不同,以下標準定義了不同的結構來對秘鑰數據進行 ASN.1 編碼。通常而言,不同格式的秘鑰暗示了不同的結構。

  • **pkcs#1 **用于定義 RSA 公鑰、私鑰結構
  • pkcs#7 用于定義證書鏈
  • **pkcs#8 **用于定義任何算法公私鑰
  • **pkcs#12 **用于定義私鑰證書
  • X.509 定義公鑰證書

這些格式的具體區別比較參見下文3.5.2

3.5 表現層

可以看到 ASN.1 及其編碼規則(BER, CER, DER)定義的是二進制規則,保存在文件中也是二進制格式。由于當時的電子郵件標準不支持二進制內容的傳輸,如果秘鑰文件通過電子郵件傳輸,就需要將二進制文件轉換成文本文件。這就是 PEM(Privacy-Enhanced Mail, 私密增強郵件)的由來。因此,PEM 文件中保存的秘鑰內容是 ASN.1 編碼生成的二進制內容,再進行 base64 編碼后的文本。

另外,為了方便用戶識別是何種格式,中文件的首尾加上一行表示身份的文本。PEM 文件一般包含三部分:首行標簽,BASE64 編碼的文本數據,尾行標簽。

-----BEGIN <label>-----
<BASE64 ENCODED DATA>
-----END <label>-----

針對不同的格式,<label> 值不一樣。

3.5.2 PEM 文件格式小結

[圖片上傳失敗...(image-b68b57-1611021868265)]

3.6 應用層

在實際使用中,不僅僅需要使用公私鑰對數據進行加解密,還需要根據不同的使用場景,解決密鑰的分發、驗證等。第5節列舉了RSA密鑰的一些常見使用場景。

四、工具

4.1 openssl

注意:下面的命令中-RSAPublicKey_in, -RSAPublicKey_out選項需要openssl1.0以上版本支持,如果報錯,請檢查 openssl 版本。

4.1.1 創建秘鑰文件

# 生成 pkcs#1 格式2048位的私鑰
openssl genrsa -out private.pem 2048
 
#從私鑰中提取 pkcs#8 格式公鑰
openssl rsa -in private.pem -out public.pem -pubout
 
#從私鑰中提取 pkcs#1 格式公鑰
openssl rsa -in private.pem -out public.pem -RSAPublicKey_out

4.1.2 秘鑰文件格式轉換

#pkcs#1 公鑰轉換成 pkcs#8 公鑰
openssl rsa -in public.pem -out public-pkcs8.pem -RSAPublicKey_in
 
#pkcs#8 公鑰轉換成 pkcs#1 公鑰
openssl rsa -in public-pkcs8.pem -out public-pkcs1.pem -pubin -RSAPublicKey_out
 
#pkcs#1 私鑰轉換成 pkcs#8 私鑰
openssl pkcs8 -in private.pem -out private-pkcs8.pem -topk8
 
#pkcs#8 私鑰轉換成 pkcs#1 私鑰
openssl rsa -in private-pkcs8.pem -out private-pkcs1.pem

4.1.3 查看秘鑰文件信息

#查看公鑰信息
openssl rsa -in public.pem -pubin -text -noout
 
#查看私鑰信息
openssl rsa -in private.pem -text -noout

4.1.4 證書

RSA證書

#從現有私鑰創建 CSR 文件
openssl req -key private.pem -out request.csr -new
 
#從現有 CSR 文件和私鑰中創建證書,有效期365天
openssl x509 -req -in request.csr -signkey private.pem -out cert.crt -days 365
 
#生成全新證書和私鑰
openssl req -nodes -newkey rsa:2048 -keyout root.key -out root.crt -x509 -days 365
 
#通 過 現 有 證 書 和 私 鑰 (作 為CA ) 為 其 他 CSR 文 件 簽 名
openssl x509 -req -in child.csr -days 365 -CA root.crt -CAkey root.key -set_serial 01 -out child.crt
 
#查看證書信息
openssl x509 -in child.crt -text -noout
 
 
#從證書中提取公鑰
openssl x509 -pubkey -noout -in child.crt  > public.pem

4.1.5 JKS

#將CA證書轉換成JKS格式
keytool -importcert -alias Cacert -file ca.crt  -keystore truststoremysql.jks -storepass password123
 
#將client.crt和client.key轉換成PKCS#12格式
openssl pkcs12 -export -in client.crt -inkey client.key -name "mysqlclient" -passout pass:mypassword -out client-keystore.p12
 
#將PKCS#12格式轉換成JKS格式
keytool -importkeystore -srckeystore client-keystore.p12 -srcstoretype pkcs12 -srcstorepass mypassword -destkeystore clientstore.jks -deststoretype JKS -deststorepass password456

五、 RSA密鑰使用場景

5.1 HTTPS單向認證

由于HTTP協議是明文傳輸,為了保證HTTP報文不被泄露和篡改,HTTPS通過SSL/TLS協議對HTTP報文進行加解密。

簡單來說,HTTPS協議要求客戶端和服務端建立連接的過程中,首先進行會話密鑰交換,然后使用該會話密鑰對通信報文進行加解密。整個通信過程如下:

  1. 服務端通過4.1.4所示方法創建RSA證書server.crt和私鑰server.key,并在WEB服務器中進行配置。

  2. 客戶端與服務端建立連接,服務端向客戶端發送證書server.crt。

  3. 客戶端對服務端證書進行校驗,并隨機生成會話密鑰,將通過服務端證書對會話密鑰進行加密,傳給服務端。

  4. 服務端通過server.key對加密后的會話密鑰進行解密,獲得會話密鑰原文。

  5. 客戶端通過會話密鑰對HTTP報文進行加密,傳給服務端。

  6. 服務端通過會話密鑰對HTTP加密報文進行解密,獲得HTTP報文原文。

  7. 服務端通過會話密鑰對HTTP響應報文進行加密,返回給客戶端。

  8. 客戶端通過會話密鑰對HTTP響應報文進行解密,獲得HTTP響應報文原文。

image

(圖1. HTTPS單向認證)

5.2 HTTPS雙向認證

5.1節描述的HTTPS場景是一個通用場景,整個過程只有客戶端對于服務端的驗證,即客戶端拿到服務端的證書后,會對證書進行有效性驗證,比如是否是CA簽名的,是否仍處于有效期內等。這種單向驗證在瀏覽器訪問等場景中沒有問題,因為這種服務設計地目的就是對外數以萬計的用戶提供服務。但是在某些場景,比如說僅對特定企業、商戶提供服務,服務端需要對客戶端進行驗證,通過驗證的受信客戶端才能正常。

訪問服務端時,就需要用到HTTPS雙向認證。

HTTPS雙向認證的過程,就是在HTTPS單向認證的基礎之上,增進服務端對客戶端的認證。解決方案的思路就是,客戶端保存客戶端證書client.crt,但是客戶端證書不是客戶端自己簽名或者CA簽名,而是由服務端的root.key進行簽名。在HTTPS雙向認證過程中,客戶端需要將客戶端證書client.crt發送給服務端,服務端使用root.key進行驗證無誤后,方可進行后續通信;否則,該客戶端即非受信客戶端,服務端拒絕提供后續服務。

具體通信過程如下所示:

  1. 服務端通過4.1.4所示方法創建RSA證書server.crt和私鑰server.key,并在WEB服務器中進行配置。

  2. 客戶端與服務端建立連接,服務端向客戶端發送證書server.crt。

  3. 客戶端對服務端證書進行校驗,驗證通過后繼續后續流程;驗證不通過則斷開連接,流程結束。

  4. 服務端向客戶端發送報文,請求客戶端發送客戶端證書。

  5. 客戶端向服務端發送客戶端證書。

  6. 服務端通過root.key對客戶端證書進行驗證,驗證無誤進行后續流程;否則斷開連接,流程結束。

  7. 客戶端隨機生成會話密鑰,將通過服務端證書對會話密鑰進行加密,傳給服務端。

  8. 服務端通過server.key對加密后的會話密鑰進行解密,獲得會話密鑰原文。

  9. 客戶端通過會話密鑰對HTTP報文進行加密,傳給服務端。

  10. 服務端通過會話密鑰對HTTP加密報文進行解密,獲得HTTP報文原文。

  11. 服務端通過會話密鑰對HTTP響應報文進行加密,返回給客戶端。

  12. 客戶端通過會話密鑰對HTTP響應報文進行解密,獲得HTTP響應報文原文。

可以看出,向較于HTTPS單向認證過程,HTTPS雙向認證過程在客戶端驗證服務端證書之后,在向服務端發送加密的會話密鑰之前,會增加客戶端向服務端發送客戶端證書client.crt,服務端對該證書進行驗證的過程。

[圖片上傳失敗...(image-65cc35-1611021868265)]

(圖2. HTTPS雙向認證)

5.3 MySQL開啟 SSL

MySQL提供SSL的原理,與HTTPS類似,不同之處在于MySQL提供的服務的對象不會是成千上萬的普通用戶,因此對于CA的需求并不高。

因此實際CA證書通常都是服務端自己生成。

與HTTPS類似,MySQL提供兩種形式的SSL認證機制:單向認證和雙向認證。

5.3.1 MySQL的SSL單向認證

(1)服務端配置文件:ca.crt, server.crt, server.key,其中server.crt由ca.crt簽名生成。

(2)客戶端配置文件:ca.crt,ca.crt與服務端的ca.crt相同。

(3)客戶端生成JKS文件

keytool -importcert -alias Cacert -file ca.crt -keystore truststoremysql.jks -storepass password123

(4)通過jdbc字符串配置SSL選項和JKS文件

verifyServerCertificate=true&useSSL=true&requireSSL=true&trustCertificateKeyStoreUrl=file:./truststoremysql.jks&trustCertificateKeyStorePassword=password123

5.3.2 MySQL的SSL雙向認證

(1)服務端配置文件:ca.crt, server.crt, server.key, 其中server.crt由ca.crt簽名生成。

(2)客戶端配置文件:ca.crt, client.crt, client.key, 其中ca.crt與服務端的ca.crt相同, client.crt由ca.crt簽名生成。

(3)客戶端生成trustKeyStore文件

keytool -importcert -alias Cacert -file ca.crt -keystore truststore.jks -storepass password123

(4)客戶端生成clientKeyStore文件

keytool -importcert -alias Cacert -file ca.crt -keystore clientstore.jks -storepass password456

(5)通過jdbc字符串配置SSL選項和JKS文件

verifyServerCertificate=true&useSSL=true&requireSSL=true&trustCertificateKeyStoreUrl=file:./truststore.jks&trustCertificateKeyStorePassword=password123&clientCertificateKeyStoreUrl=file:./clientstore.jks&clientCertificateKeyStorePassword=password456

關于MySQL的SSL認證更多細節可以參考:

附錄A 不同格式的 ASN.1 編碼

A.1 pkcs#1

A.1.1 公鑰

RSAPublicKey ::= SEQUENCE {
    modulus INTEGER , -- n
    publicExponent INTEGER -- e
}

A.1.2 私鑰

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
}

A.2 pkcs#8

A.2.1 pkcs#8 公鑰

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

A.2.2 pkcs#8 私鑰

OneAsymmetricKey ::= SEQUENCE {
    version Version ,
    privateKeyAlgorithm PrivateKeyAlgorithmIdentifier ,
    privateKey PrivateKey ,
    attributes [0] Attributes OPTIONAL ,
    ...,
    [[2: publicKey [1] PublicKey OPTIONAL ]],
    ...
}
PrivateKey ::= OCTET STRING
    -- Content varies based on type of key. The
    -- algorithm identifier dictates the format of
    -- the key.

A.3 X.509

A.3.1 X.509 證書

Certificate ::= SEQUENCE {
    tbsCertificate TBSCertificate ,
    signatureAlgorithm AlgorithmIdentifier ,
    signatureValue BIT STRING
}
 
TBSCertificate ::= SEQUENCE {
    version [0] EXPLICIT 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] EXPLICIT Extensions OPTIONAL
        -- If present , version MUST be v3
}
 
Version ::= INTEGER { v1(0), v2(1), v3(2) }
 
CertificateSerialNumber ::= INTEGER
 
Validity ::= SEQUENCE {
    notBefore Time,
    notAfter Time
}
 
Time ::= CHOICE {
    utcTime UTCTime ,
    generalTime GeneralizedTime
}
 
 
UniqueIdentifier ::= BIT STRING
 
SubjectPublicKeyInfo ::= SEQUENCE {
    algorithm AlgorithmIdentifier ,
    subjectPublicKey BIT STRING
}
 
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
 
Extension ::= SEQUENCE {
    extnID OBJECT IDENTIFIER ,
    critical BOOLEAN DEFAULT FALSE ,
    extnValue OCTET STRING
        -- contains the DER encoding of an ASN.1 value
        -- corresponding to the extension type identified
        -- by extnID
}

作者:Zhu Ran ,來自vivo互聯網技術團隊

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容