查看源代码 示例

本节介绍如何使用 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-TypeDEK-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 = "[email protected]"}]]},
            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 = "[email protected]"}]]},
            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,"[email protected]"}]}]},
    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/2pem_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),

注意

您通常只执行加密或解密操作中的一个,而对等方执行另一个。这通常在旧版应用程序中用作原始数字签名。

警告

尽管在使用带有 Erlang/OTP 的适当 OpenSSL 密码库时存在软件预防措施,但此旧版算法已被破坏,很难保证安全性,我们强烈建议不要使用它。

数字签名

假设您有以下私钥和相应的公钥

  • PrivateKey = #'RSAPrivateKey{}'#'DSAPrivateKey'{} 和明文 Msg = binary()
  • PublicKey = #'RSAPublicKey'{}{integer(), #'DssParams'{}}

然后您可以按如下步骤进行

Signature = public_key:sign(Msg, sha, PrivateKey),
true = public_key:verify(Msg, sha, Signature, PublicKey),

注意

您通常只执行签名或验证操作中的一个,而对等方执行另一个。

在调用 signverify 之前计算消息摘要,然后使用 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 中实现。客户端可以使用选项列表挂钩修改后的规则。

需要一些术语:证书展示它有效的 hostname(s)。这些称为展示 ID。客户端认为它连接到的 hostname(s) 称为参考 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.combar.example.com 都匹配,但不匹配 foo.bar.example.com。名称 erlang.org 也不匹配,因为它不是 CN。

如果展示 ID 是从 Subject 证书字段获取的,则名称可能包含通配符。该函数按照 RFC 6125 第 6.4.3 章中的定义处理此问题。

可能只有一个通配符,并且它位于第一个标签中,例如:*.example.com。这匹配 foo.example.com,但不匹配 example.comfoo.bar.example.com

通配符之前或/和之后可能存在标签字符。例如:a*d.example.com 匹配 abcd.example.comad.example.com,但不匹配 ab.cd.example.com

在之前的示例中,没有指示期望的协议。因此,客户端无法指示它连接的是 Web 服务器、ldap 服务器还是 sip 服务器。证书中有可以指示这一点的字段。更准确地说,rfc 引入了 X509v3 Subject Alternative NameX509v3 extensions 字段中的用法

 $ 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 提供安全的 Web 服务器。

下一个示例同时存在 SubjectSubject 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.orghttps://www.example.org 匹配。example.comfoo.example.com 的匹配都失败,因为它们位于未检查的 Subject 字段中,因为 Subject Alternate Name 字段存在。

函数调用示例

注意

像 ssl/tls 或 https 这样的其他应用程序可能具有传递给 public_key:pkix_verify_hostname 的选项。您可能不必直接调用它

假设我们的客户端希望连接到 Web 服务器 https://www.example.net。因此,此 URI 是客户端的参考 ID。调用将是

 public_key:pkix_verify_hostname(CertFromHost,
                                 [{uri_id, "https://www.example.net"}
                                 ]).

调用将根据检查返回 truefalse。调用者不需要处理 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_funmatch_fun 来完成。

主机名提取

fqdn_fun 从 uri_id 或其他在 public_key 函数中未预定义的 ReferenceIDs 中提取主机名(完全限定域名)。假设您有一个具有非常特殊协议部分的 URI:myspecial://example.com"。由于这是一个非标准 URI,因此不会提取用于匹配 Subject 中 CN 名称的主机名。

要“教”函数如何提取,您可以提供一个函数来替换默认的提取函数。fqdn_fun 接收一个参数,并返回一个要与每个 CN 名称匹配的 string/0 或原子 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 接收两个参数,并返回 truefalsedefault。值 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 字段的 Presented IDs 设置单独的匹配规则。

默认匹配在比较之前将字符串中的 ASCII 值转换为小写。但是,调用 match_fun 时,不会对字符串应用任何转换。原因是使用户能够对需要原始格式的字符串进行不可预见的处理。

“固定”证书

RFC 6125固定定义为

“在应用程序服务的证书和客户端的某个引用标识符之间建立缓存的名称关联的行为,尽管没有一个呈现的标识符与给定的引用标识符匹配。...”

目的是提供一种机制,让人类接受一个原本有错误的证书。例如,在 Web 浏览器中,您可能会收到如下问题:

警告:您想访问网站 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}]),
 ...