檢視原始碼 範例
本節說明如何使用 Public Key API 的範例。以下各節中使用的金鑰和憑證僅用於測試 Public Key 應用程式。
為了提高可讀性,以下範例中的一些 Shell 輸出已縮寫。
PEM 檔案
公鑰資料(金鑰、憑證等等)可以儲存在 Privacy Enhanced Mail (PEM) 格式中。PEM 檔案具有以下結構
<text>
-----BEGIN <SOMETHING>-----
<Attribute> : <Value>
<Base64 encoded DER data>
-----END <SOMETHING>-----
<text>
一個檔案可以包含多個 BEGIN/END
區塊。區塊之間的文字行會被忽略。屬性(如果存在)會被忽略,但 Proc-Type
和 DEK-Info
除外,它們會在 DER
資料被加密時使用。
DSA 私鑰
DSA 私鑰可以如下所示
注意
檔案處理不是由 Public Key 應用程式完成的。
1> {ok, PemBin} = file:read_file("dsa.pem").
{ok,<<"-----BEGIN DSA PRIVATE KEY-----\nMIIBuw"...>>}
以下 PEM 檔案只有一個條目,一個私有 DSA 金鑰
2>[DSAEntry] = public_key:pem_decode(PemBin).
[{'DSAPrivateKey',<<48,130,1,187,2,1,0,2,129,129,0,183,
179,230,217,37,99,144,157,21,228,204,
162,207,61,246,...>>,
not_encrypted}]
3> Key = public_key:pem_entry_decode(DSAEntry).
#'DSAPrivateKey'{version = 0,
p = 12900045185019966618...6593,
q = 1216700114794736143432235288305776850295620488937,
g = 10442040227452349332...47213,
y = 87256807980030509074...403143,
x = 510968529856012146351317363807366575075645839654}
帶有密碼的 RSA 私鑰
使用密碼加密的 RSA 私鑰可以如下所示
1> {ok, PemBin} = file:read_file("rsa.pem").
{ok,<<"Bag Attribute"...>>}
以下 PEM 檔案只有一個條目,一個私有 RSA 金鑰
2>[RSAEntry] = public_key:pem_decode(PemBin).
[{'RSAPrivateKey',<<224,108,117,203,152,40,15,77,128,126,
221,195,154,249,85,208,202,251,109,
119,120,57,29,89,19,9,...>>,
{"DES-EDE3-CBC",<<"kÙeø¼pµL">>}}]
在以下範例中,密碼是 "abcd1234"
3> Key = public_key:pem_entry_decode(RSAEntry, "abcd1234").
#'RSAPrivateKey'{version = 'two-prime',
modulus = 1112355156729921663373...2737107,
publicExponent = 65537,
privateExponent = 58064406231183...2239766033,
prime1 = 11034766614656598484098...7326883017,
prime2 = 10080459293561036618240...77738643771,
exponent1 = 77928819327425934607...22152984217,
exponent2 = 36287623121853605733...20588523793,
coefficient = 924840412626098444...41820968343,
otherPrimeInfos = asn1_NOVALUE}
X509 憑證
以下是 X509 憑證的範例
1> {ok, PemBin} = file:read_file("cacerts.pem").
{ok,<<"-----BEGIN CERTIFICATE-----\nMIIC7jCCAl"...>>}
以下檔案包含兩個憑證
2> [CertEntry1, CertEntry2] = public_key:pem_decode(PemBin).
[{'Certificate',<<48,130,2,238,48,130,2,87,160,3,2,1,2,2,
9,0,230,145,97,214,191,2,120,150,48,13,
...>>,
not_encrypted},
{'Certificate',<<48,130,3,200,48,130,3,49,160,3,2,1,2,2,1,
1,48,13,6,9,42,134,72,134,247,...>>,
not_encrypted}]
憑證可以像平常一樣解碼
2> Cert = public_key:pem_entry_decode(CertEntry1).
#'Certificate'{
tbsCertificate =
#'TBSCertificate'{
version = v3,serialNumber = 16614168075301976214,
signature =
#'AlgorithmIdentifier'{
algorithm = {1,2,840,113549,1,1,5},
parameters = <<5,0>>},
issuer =
{rdnSequence,
[[#'AttributeTypeAndValue'{
type = {2,5,4,3},
value = <<19,8,101,114,108,97,110,103,67,65>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,11},
value = <<19,10,69,114,108,97,110,103,32,79,84,80>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,10},
value = <<19,11,69,114,105,99,115,115,111,110,32,65,66>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,7},
value = <<19,9,83,116,111,99,107,104,111,108,109>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,6},
value = <<19,2,83,69>>}],
[#'AttributeTypeAndValue'{
type = {1,2,840,113549,1,9,1},
value = <<22,22,112,101,116,101,114,64,101,114,...>>}]]},
validity =
#'Validity'{
notBefore = {utcTime,"080109082929Z"},
notAfter = {utcTime,"080208082929Z"}},
subject =
{rdnSequence,
[[#'AttributeTypeAndValue'{
type = {2,5,4,3},
value = <<19,8,101,114,108,97,110,103,67,65>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,11},
value = <<19,10,69,114,108,97,110,103,32,79,84,80>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,10},
value = <<19,11,69,114,105,99,115,115,111,110,32,...>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,7},
value = <<19,9,83,116,111,99,107,104,111,108,...>>}],
[#'AttributeTypeAndValue'{
type = {2,5,4,6},
value = <<19,2,83,69>>}],
[#'AttributeTypeAndValue'{
type = {1,2,840,113549,1,9,1},
value = <<22,22,112,101,116,101,114,64,...>>}]]},
subjectPublicKeyInfo =
#'SubjectPublicKeyInfo'{
algorithm =
#'AlgorithmIdentifier'{
algorithm = {1,2,840,113549,1,1,1},
parameters = <<5,0>>},
subjectPublicKey =
{0,<<48,129,137,2,129,129,0,203,209,187,77,73,231,90,...>>}},
issuerUniqueID = asn1_NOVALUE,
subjectUniqueID = asn1_NOVALUE,
extensions =
[#'Extension'{
extnID = {2,5,29,19},
critical = true,
extnValue = [48,3,1,1,255]},
#'Extension'{
extnID = {2,5,29,15},
critical = false,
extnValue = [3,2,1,6]},
#'Extension'{
extnID = {2,5,29,14},
critical = false,
extnValue = [4,20,27,217,65,152,6,30,142|...]},
#'Extension'{
extnID = {2,5,29,17},
critical = false,
extnValue = [48,24,129,22,112,101,116,101|...]}]},
signatureAlgorithm =
#'AlgorithmIdentifier'{
algorithm = {1,2,840,113549,1,1,5},
parameters = <<5,0>>},
signature =
<<163,186,7,163,216,152,63,47,154,234,139,73,154,96,120,
165,2,52,196,195,109,167,192,...>>}
憑證的部分可以使用 public_key:der_decode/2
解碼,使用該部分的 ASN.1 類型。然而,特定於應用程式的憑證擴展需要特定於應用程式的 ASN.1 解碼/編碼函式。在最近的範例中,rdnSequence
的第一個值是 ASN.1 類型 'X520CommonName'。({2,5,4,3} = ?id-at-commonName)
public_key:der_decode('X520CommonName', <<19,8,101,114,108,97,110,103,67,65>>).
{printableString,"erlangCA"}
然而,憑證也可以使用 pkix_decode_cert/2
解碼,它可以自訂並遞迴地解碼憑證的標準部分
3> {_, DerCert, _} = CertEntry1.
4> public_key:pkix_decode_cert(DerCert, otp).
#'OTPCertificate'{
tbsCertificate =
#'OTPTBSCertificate'{
version = v3,serialNumber = 16614168075301976214,
signature =
#'SignatureAlgorithm'{
algorithm = {1,2,840,113549,1,1,5},
parameters = 'NULL'},
issuer =
{rdnSequence,
[[#'AttributeTypeAndValue'{
type = {2,5,4,3},
value = {printableString,"erlangCA"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,11},
value = {printableString,"Erlang OTP"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,10},
value = {printableString,"Ericsson AB"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,7},
value = {printableString,"Stockholm"}}],
[#'AttributeTypeAndValue'{type = {2,5,4,6},value = "SE"}],
[#'AttributeTypeAndValue'{
type = {1,2,840,113549,1,9,1},
value = "peter@erix.ericsson.se"}]]},
validity =
#'Validity'{
notBefore = {utcTime,"080109082929Z"},
notAfter = {utcTime,"080208082929Z"}},
subject =
{rdnSequence,
[[#'AttributeTypeAndValue'{
type = {2,5,4,3},
value = {printableString,"erlangCA"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,11},
value = {printableString,"Erlang OTP"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,10},
value = {printableString,"Ericsson AB"}}],
[#'AttributeTypeAndValue'{
type = {2,5,4,7},
value = {printableString,"Stockholm"}}],
[#'AttributeTypeAndValue'{type = {2,5,4,6},value = "SE"}],
[#'AttributeTypeAndValue'{
type = {1,2,840,113549,1,9,1},
value = "peter@erix.ericsson.se"}]]},
subjectPublicKeyInfo =
#'OTPSubjectPublicKeyInfo'{
algorithm =
#'PublicKeyAlgorithm'{
algorithm = {1,2,840,113549,1,1,1},
parameters = 'NULL'},
subjectPublicKey =
#'RSAPublicKey'{
modulus =
1431267547247997...37419,
publicExponent = 65537}},
issuerUniqueID = asn1_NOVALUE,
subjectUniqueID = asn1_NOVALUE,
extensions =
[#'Extension'{
extnID = {2,5,29,19},
critical = true,
extnValue =
#'BasicConstraints'{
cA = true,pathLenConstraint = asn1_NOVALUE}},
#'Extension'{
extnID = {2,5,29,15},
critical = false,
extnValue = [keyCertSign,cRLSign]},
#'Extension'{
extnID = {2,5,29,14},
critical = false,
extnValue = [27,217,65,152,6,30,142,132,245|...]},
#'Extension'{
extnID = {2,5,29,17},
critical = false,
extnValue = [{rfc822Name,"peter@erix.ericsson.se"}]}]},
signatureAlgorithm =
#'SignatureAlgorithm'{
algorithm = {1,2,840,113549,1,1,5},
parameters = 'NULL'},
signature =
<<163,186,7,163,216,152,63,47,154,234,139,73,154,96,120,
165,2,52,196,195,109,167,192,...>>}
此呼叫等同於 public_key:pem_entry_decode(CertEntry1)
5> public_key:pkix_decode_cert(DerCert, plain).
#'Certificate'{ ...}
將公鑰資料編碼為 PEM 格式
如果您有公鑰資料並想建立 PEM 檔案,則可以呼叫函式 public_key:pem_entry_encode/2
和 pem_encode/1
,並將結果儲存到檔案中。例如,假設您有 PubKey = 'RSAPublicKey'{}
。然後,您可以建立 PEM-"RSA PUBLIC KEY" 檔案(ASN.1 類型 'RSAPublicKey'
)或 PEM-"PUBLIC KEY" 檔案('SubjectPublicKeyInfo'
ASN.1 類型)。
PEM 條目的第二個元素是 ASN.1 DER
編碼的金鑰資料
1> PemEntry = public_key:pem_entry_encode('RSAPublicKey', RSAPubKey).
{'RSAPublicKey', <<48,72,...>>, not_encrypted}
2> PemBin = public_key:pem_encode([PemEntry]).
<<"-----BEGIN RSA PUBLIC KEY-----\nMEgC...>>
3> file:write_file("rsa_pub_key.pem", PemBin).
ok
或
1> PemEntry = public_key:pem_entry_encode('SubjectPublicKeyInfo', RSAPubKey).
{'SubjectPublicKeyInfo', <<48,92...>>, not_encrypted}
2> PemBin = public_key:pem_encode([PemEntry]).
<<"-----BEGIN PUBLIC KEY-----\nMFw...>>
3> file:write_file("pub_key.pem", PemBin).
ok
RSA 公鑰密碼學
假設您有以下私鑰和對應的公鑰
PrivateKey = #'RSAPrivateKey{}'
和純文字Msg = binary()
PublicKey = #'RSAPublicKey'{}
然後您可以按如下方式進行
使用私鑰加密
RsaEncrypted = public_key:encrypt_private(Msg, PrivateKey),
Msg = public_key:decrypt_public(RsaEncrypted, PublicKey),
使用公鑰加密
RsaEncrypted = public_key:encrypt_public(Msg, PublicKey),
Msg = public_key:decrypt_private(RsaEncrypted, PrivateKey),
注意
您通常只執行加密或解密操作中的一個,而對方則執行另一個。這通常在舊版應用程式中用作原始數位簽章。
警告
雖然在使用適當的 OpenSSL 加密庫和 Erlang/OTP 時存在軟體預防措施,但此舊版演算法已損壞,很難保證安全性,我們強烈建議不要使用它。
數位簽章
假設您有以下私鑰和對應的公鑰
PrivateKey = #'RSAPrivateKey{}'
或#'DSAPrivateKey'{}
和純文字Msg = binary()
PublicKey = #'RSAPublicKey'{}
或{integer(), #'DssParams'{}}
然後您可以按如下方式進行
Signature = public_key:sign(Msg, sha, PrivateKey),
true = public_key:verify(Msg, sha, Signature, PublicKey),
注意
您通常只執行簽署或驗證操作中的一個,而對方則執行另一個。
在呼叫 sign
或 verify
之前計算訊息摘要,然後使用 none
作為第二個參數可能是適當的
Digest = crypto:sha(Msg),
Signature = public_key:sign(Digest, none, PrivateKey),
true = public_key:verify(Digest, none, Signature, PublicKey),
驗證憑證主機名稱
背景
當客戶端檢查伺服器憑證時,會有多種檢查可用,例如檢查憑證是否被撤銷、偽造或過期。
但是,有些攻擊是這些檢查無法偵測到的。假設一個壞人成功地進行了 DNS 感染。然後客戶端可能會認為它正在連接到一個主機,但最終卻連接到另一個邪惡的主機。雖然它是邪惡的,但它可能擁有完全合法的憑證!該憑證具有有效的簽章,未被撤銷,憑證鏈沒有偽造,並且具有受信任的根憑證等等。
為了偵測伺服器是否不是預期的伺服器,客戶端必須額外執行主機名稱驗證。此程序在 RFC 6125 中描述。其概念是憑證列出可以從中取得憑證的主機名稱。這會在簽署憑證時由憑證頒發者檢查。因此,如果憑證是由受信任的根憑證頒發的,則客戶端可以信任其中簽署的主機名稱。
在 RFC 6125 第 6 節中定義了一個預設的主機名稱比對程序,以及在 RFC 6125 附錄 B 中定義的協議相關變體。預設程序在 public_key:pkix_verify_hostname/2,3 中實作。客戶端可以使用選項清單掛鉤修改的規則。
需要一些術語:憑證會呈現它有效的 (多個) 主機名稱。這些稱為呈現 ID。客戶端認為它連接到的 (多個) 主機名稱稱為參考 ID。比對規則旨在驗證是否存在至少一個與其中一個呈現 ID 相符的參考 ID。如果沒有,則驗證失敗。
ID 包含一般完整網域名稱,例如 foo.example.com
,但不建議使用 IP 位址。rfc 描述了為什麼不建議這樣做,以及關於如何取得參考 ID 的安全性考量。
不支援國際化網域名稱。
驗證過程
傳統上,呈現 ID 在 Subject
憑證欄位中以 CN
名稱找到。這仍然很常見。當列印憑證時,它們會顯示為
$ openssl x509 -text < cert.pem
...
Subject: C=SE, CN=example.com, CN=*.example.com, O=erlang.org
...
範例 Subject
欄位有一個 C、兩個 CN 和一個 O 部分。只有 CN(通用名稱)用於主機名稱驗證。其他兩個(C 和 O)在這裡不使用,即使它們包含像 O 部分一樣的網域名稱。C 和 O 部分在其他地方定義,並且僅對其他函式有意義。
在範例中,呈現 ID 是 example.com
以及與 *.example.com
相符的主機名稱。例如,foo.example.com
和 bar.example.com
都相符,但 foo.bar.example.com
不相符。名稱 erlang.org
都不相符,因為它不是 CN。
如果呈現 ID 是從 Subject
憑證欄位擷取的,則名稱可能包含萬用字元。此函式會按照 RFC 6125 中的 6.4.3 章中的定義處理此問題。
只能有一個萬用字元,它位於第一個標籤中,例如:*.example.com
。這與 foo.example.com
相符,但與 example.com
或 foo.bar.example.com
都不相符。
萬用字元之前或/和之後可以有標籤字元。例如:a*d.example.com
與 abcd.example.com
和 ad.example.com
相符,但與 ab.cd.example.com
不相符。
在先前的範例中,沒有指示預期的協定。因此,客戶端沒有指示它連接到的是網頁伺服器、ldap 伺服器還是 sip 伺服器。憑證中有欄位可以指示這一點。更確切地說,rfc 引入了在 X509v3 extensions
欄位中使用 X509v3 Subject Alternative Name
$ openssl x509 -text < cert.pem
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:kb.example.org, URI:https://www.example.org
...
在這裡,kb.example.org
服務於任何協定,而 www.example.org
呈現安全的網頁伺服器。
下一個範例同時存在 Subject
和 Subject Alternate Name
$ openssl x509 -text < cert.pem
...
Subject: C=SE, CN=example.com, CN=*.example.com, O=erlang.org
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:kb.example.org, URI:https://www.example.org
...
RFC 聲明,如果憑證在 Subject Alternate Name
欄位中定義了參考 ID,則在進行主機名稱檢查時,不得使用 Subject
欄位,即使它包含有效的 CN 名稱。因此,只有 kb.example.org
和 https://www.example.org
相符。對於 example.com
和 foo.example.com
來說,比對都會失敗,因為它們位於 Subject
欄位中,而由於 Subject Alternate Name
欄位存在,因此不會檢查該欄位。
函式呼叫範例
注意
其他應用程式(如 ssl/tls 或 https)可能具有傳遞到
public_key:pkix_verify_hostname
的選項。您可能不必直接呼叫它
假設我們的客戶端希望連接到網頁伺服器 https://www.example.net。因此,此 URI 是客戶端的參考 ID。呼叫將會是
public_key:pkix_verify_hostname(CertFromHost,
[{uri_id, "https://www.example.net"}
]).
呼叫將根據檢查傳回 true
或 false
。呼叫者不需要處理 rfc 中的比對規則。比對將按如下方式進行
- 如果存在
Subject Alternate Name
欄位,則函式呼叫中的{uri_id,string()}
將與憑證欄位中的任何{uniformResourceIdentifier,string()}
進行比較。如果兩個strings()
相等(不區分大小寫),則表示比對成功。相同的情況適用於呼叫中的任何{dns_id,string()}
,它會與憑證欄位中的所有{dNSName,string()}
進行比較。 - 如果沒有
Subject Alternate Name
欄位,則會檢查Subject
欄位。所有CN
名稱都會與從{uri_id,string()}
和{dns_id,string()}
提取的所有主機名稱進行比較。
擴展搜尋機制
呼叫者可以使用自己的提取和匹配規則。這可以透過兩個選項 fqdn_fun
和 match_fun
來完成。
主機名稱提取
fqdn_fun
從 uri_id 或其他在 public_key 函數中沒有預先定義的 ReferenceID 中提取主機名稱(完整網域名稱)。假設您有一些具有非常特殊協議部分的 URI:myspecial://example.com"
。由於這是一個非標準 URI,因此不會提取任何主機名稱來匹配 Subject
中的 CN 名稱。
為了「教導」該函數如何提取,您可以提供一個函數來取代預設的提取函數。fqdn_fun
接受一個參數,並返回一個 string/0
以匹配每個 CN 名稱,或者返回原子 default
,這將調用預設的 fqdn 提取函數。返回值 undefined
會從 fqdn 提取中移除目前的 URI。
...
Extract = fun({uri_id, "myspecial://"++HostName}) -> HostName;
(_Else) -> default
end,
...
public_key:pkix_verify_hostname(CertFromHost, RefIDs,
[{fqdn_fun, Extract}])
...
重新定義匹配操作
預設匹配處理 dns_id 和 uri_id。在 uri_id 中,會測試該值是否與 Subject Alternate Name
中的值相等。如果需要其他類型的匹配,請使用 match_fun
選項。
match_fun
接受兩個參數,並返回 true
、false
或 default
。值 default
將調用預設的匹配函數。
...
Match = fun({uri_id,"myspecial://"++A},
{uniformResourceIdentifier,"myspecial://"++B}) ->
my_match(A,B);
(_RefID, _PresentedID) ->
default
end,
...
public_key:pkix_verify_hostname(CertFromHost, RefIDs,
[{match_fun, Match}]),
...
在 ReferenceID 和 Subject
欄位中的 CN 值之間進行匹配操作時,該函數的第一個參數是從 ReferenceID 提取的主機名稱,第二個參數是從 Subject
欄位取得的元組 {cn, string()}
。這使得可以為 Subject
欄位和 Subject Alternate Name
欄位中呈現的 ID 設定單獨的匹配規則。
預設匹配會在比較之前將字串中的 ASCII 值轉換為小寫。然而,match_fun
會在沒有對字串套用任何轉換的情況下被呼叫。原因是要讓使用者能夠在需要原始格式的情況下,對字串進行未預見的處理。
「釘選」憑證
RFC 6125 將釘選定義為
「在應用程式服務的憑證與客戶端的參考識別符之一之間建立快取的名稱關聯的行為,即使沒有任何呈現的識別符與給定的參考識別符匹配...。」
其目的是提供一種機制,讓人類可以接受原本錯誤的憑證。例如,在網頁瀏覽器中,您可能會收到類似這樣的問題
「警告:您想要訪問 www.example.com 網站,但憑證是針對 shop.example.com。是否仍然接受(是/否)?」
這可以透過選項 fail_callback
來完成,如果主機名稱驗證失敗,將會呼叫該選項
-include_lib("public_key/include/public_key.hrl"). % Record def
...
Fail = fun(#'OTPCertificate'{}=C) ->
case in_my_cache(C) orelse my_accept(C) of
true ->
enter_my_cache(C),
true;
false ->
false
end,
...
public_key:pkix_verify_hostname(CertFromHost, RefIDs,
[{fail_callback, Fail}]),
...