3.入门 | 3. Getting Started
3 入门
本节描述如何使用公钥API的示例。以下部分中使用的密钥和证书仅用于测试公钥应用程序。
下面的例子中的一些shell打印被缩写为增加可读性。
3.1 PEM文件
公钥数据(密钥,证书等)可以保存为增强型邮件(PEM)格式。PEM文件具有以下结构:
<text>
-----BEGIN <SOMETHING>-----
<Attribute> : <Value>
<Base64 encoded DER data>
-----END <SOMETHING>-----
<text>
一个文件可以包含多个BEGIN/END
块。块之间的文本行被忽略。如果存在属性,则会忽略数据加密时除外的Proc-Type
和。DEK-InfoDER
DSA私钥
DSA私钥可以如下所示:
注
文件处理不是由公钥应用程序完成的。
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 Attribut"...>>}
以下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}
X 509证书
以下是X 509证书的示例:
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公钥”文件(ASN.1类型'RSAPublicKey')或PEM-“公钥”文件('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
3.2 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),
注
您通常只执行一项加密或解密操作,而对等方则执行另一项操作。这种规范作为原始数字签名在遗留应用程序中使用。
3.3 数字签名
假设您拥有以下私钥和相应的公钥:
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),
3.4 验证证书主机名
背景
当客户端检查服务器证书时,有许多可用的检查,比如检查证书没有被撤销、没有伪造或者没有过期。
但是,这些检查没有检测到一些攻击。假设一个坏蛋因DNS感染而得病。然后,客户端可以相信它是连接到一个主机,但最终在另一个但邪恶的主机。虽然它是邪恶的,但它可能有一个完全合法的证书%21证书有一个有效的签名,它没有被撤销,证书链不是伪造的,并且有一个可信的根等等。
要检测到服务器不是预期的服务器,客户端必须另外执行主机名验证
。这个过程描述在RFC 6125
。这个想法是证书列出了它可以从中获取的主机名。证书颁发者在证书签名时检查这一点。因此,如果证书是由受信任的根发布的,则客户端可以信任其中签名的主机名。
有一个默认的主机名匹配程序RFC 6125, section 6
以及定义在协议相关的变化中RFC 6125 appendix 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。
如果从Subject
证书字段获取提供的ID ,则名称可能包含通配符。该函数按照中所定义的来处理它chapter 6.4.3 in RFC 6125
。
可能只有一个通配符,并且位于第一个标签中,例如:* .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
。
在前面的例子中,没有指示预期哪些协议。因此,客户端没有任何迹象表明它是否是Web服务器,ldap服务器或连接到的sip服务器。证书中有可以指示这一点的字段。更准确地说,rfc介绍了X509v3 Subject Alternative Name
该X509v3 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服务器。
下一个例子既有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规定,如果证书在主题备用名称字段中定义引用ID,则主题字段不得用于主机名称检查,即使它包含有效的CN名称。 因此只有kb.example.org和https://www.example.org匹配。 由于主题副本名称字段存在,因此它们位于“主题”字段中,因为它没有被选中,所以对于example.com和foo.example.com,匹配都失败。
函数调用示例
注
其他应用程序(如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"}
]).
调用将返回true
或false
取决于支票。调用者不需要处理rfc中的匹配规则。匹配的过程如下:
- 如果存在主题备用名称字段,则函数调用中的{uri_id,string()}将与证书字段中的任何{uniformResourceIdentifier,string()}进行比较。 如果两个字符串()相等(不区分大小写),则匹配。 这同样适用于与证书字段中的所有{dNSName,string()}进行比较的调用中的任何{dns_id,string()}。
- 如果没有主题备用名称字段,则会检查主题字段。 所有CN名称将与从{uri_id,string()}和{dns_id,string()}提取的所有主机名进行比较。
扩展搜索机制
调用者可以使用自己的提取和匹配规则。这是通过两个选项fqdn_fun
和match_fun
。
主机名提取
fqdn_fun从uri_id或public_key函数中未预先定义的其他ReferenceID中提取主机名(完全限定的域名)。 假设你有一个非常特殊的协议部分的URI:myspecial://example.com“。因为这是一个非标准的URI,所以在主体中没有提取匹配CN名的主机名。
要“教”如何提取功能,您可以提供一种替代默认提取功能的乐趣。在fqdn_fun
有一个参数,并返回无论是string()
被匹配到每个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中,测试值是否与主题备用名称中的值相等。 如果需要其他类型的匹配,请使用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}]),
...
如果从Subject
字段中的ReferenceID和CN值之间进行匹配操作,则fun的第一个参数是从ReferenceID提取的主机名,第二个参数是{cn, string()}
从该Subject
字段中获取的元组。这使得可以为Subject
字段和Subject Alternate Name
字段中显示的ID提供单独的匹配规则。
默认匹配会在比较之前将字符串中的ascii值转换为小写。该match_fun
然而,被称为不应用于任何字符串transfomation。原因是使用户能够对需要原始格式的字符串进行未经处理的处理。
“Pinning”证书
该RFC 6125
定义pinning
为:
“在应用服务的证书和其中一个客户端的引用标识符之间建立一个缓存名称关联的行为,尽管所提供的标识符都不匹配给定的引用标识符......”
目的是为了让人们接受一个有缺陷的证书。例如在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}]),
...
3.5 SSH文件
SSH通常使用PEM文件作为私钥,但它有自己的文件格式来存储公钥。该public_key
应用程序可用于解析SSH公钥文件的内容。
RFC 4716 SSH公钥文件
RFC 4716 SSH文件看起来像PEM文件一样混乱,但有一些差异:
1> {ok, SshBin} = file:read_file("ssh2_rsa_pub").
{ok, <<"---- BEGIN SSH2 PUBLIC KEY ----\nAAAA"...>>}
这相当于调用public_key:ssh_decode(SshBin, rfc4716_public_key)
*
2> public_key:ssh_decode(SshBin, public_key).
[{#'RSAPublicKey'{modulus = 794430685...91663,
publicExponent = 35}, []}]
OpenSSH公钥格式
OpenSSH公钥格式如下:
1> {ok, SshBin} = file:read_file("openssh_dsa_pub").
{ok,<<"ssh-dss AAAAB3Nza"...>>}
这相当于调用public_key:ssh_decode(SshBin, openssh_public_key)
*
2> public_key:ssh_decode(SshBin, public_key).
[{{15642692...694280725,
#'Dss-Parms'{p = 17291273936...696123221,
q = 1255626590179665817295475654204371833735706001853,
g = 10454211196...480338645}},
[{comment,"dhopson@VMUbuntu-DSH"}]}]
已知主机-OpenSSH格式
已知主机-OpenSSH格式如下:
1> {ok, SshBin} = file:read_file("known_hosts").
{ok,<<"hostname.domain.com,192.168.0.1 ssh-rsa AAAAB...>>}
返回公钥及其相关属性的列表。每个键和属性对应于已知主机文件中的一个条目:
2> public_key:ssh_decode(SshBin, known_hosts).
[{#'RSAPublicKey'{modulus = 1498979460408...72721699,
publicExponent = 35},
[{hostnames,["hostname.domain.com","192.168.0.1"]}]},
{#'RSAPublicKey'{modulus = 14989794604088...2721699,
publicExponent = 35},
[{comment,"foo@bar.com"},
{hostnames,["|1|BWO5qDxk/cFH0wa05JLdHn+j6xQ=|rXQvIxh5cDD3C43k5DPDamawVNA="]}]}]
授权密钥-OpenSSH格式
授权密钥-OpenSSH格式如下:
1> {ok, SshBin} = file:read_file("auth_keys").
{ok, <<"command=\"dump /home\",no-pty,no-port-forwarding ssh-rsa AAA...>>}
返回公钥及其相关属性的列表。每一对密钥和属性对应于授权密钥文件中的一个条目:
2> public_key:ssh_decode(SshBin, auth_keys).
[{#'RSAPublicKey'{modulus = 794430685...691663,
publicExponent = 35},
[{comment,"dhopson@VMUbuntu-DSH"},
{options,["command=\"dump/home\"","no-pty",
"no-port-forwarding"]}]},
{{1564269258491...607694280725,
#'Dss-Parms'{p = 17291273936185...763696123221,
q = 1255626590179665817295475654204371833735706001853,
g = 10454211195705...60511039590076780999046480338645}},
[{comment,"dhopson@VMUbuntu-DSH"}]}]
从公钥数据创建SSH文件
如果你有公钥PubKey
以及相关属性列表Attributes
由ssh_decode/2
,您可以创建一个新的SSH文件,例如:
N> SshBin = public_key:ssh_encode([{PubKey, Attributes}], openssh_public_key),
<<"ssh-rsa "...>>
N+1> file:write_file("id_rsa.pub", SshBin).
ok