ECC公钥格式的详细解析(转载)
本文首先介绍公钥格式相关的若干概念/技术,随后以示例的方式剖析DER格式的ECC公钥,最后介绍如何使用Java生成、解析和使用ECC公钥。
ASN.1
Abstract Syntax Notation One (ASN.1)是一种接口描述语言,提供了一种平台无关的描述数据结构的方式。ASN.1是ITU-T、ISO、以及IEC的标准,广泛应用于电信和计算机网络领域,尤其是密码学领域。
ASN.1与大家熟悉的Protocol Buffers和Apache Thrift非常相似,都可以通过schema来定义数据结构,提供跨平台的数据序列化和反序列化能力。不同的是,ASN.1早在1984年就被定为标准,比这两者要早很多年,并得到了广泛的应用,被用来定义了很多世界范围内广泛使用的数据结构,有大量的RFC文档使用ASN.1定义协议、数据格式等。比如https所使用的X.509证书结构,就是使用ASN.1定义的。
ASN.1定义了若干基础的数据类型和结构类型:
Topic | Description |
---|---|
Basic Types | BIT STRING BOOLEAN INTEGER NULL OBJECT IDENTIFIER OCTET STRING |
String Types | BMPString IA5String PrintableString TeletexString UTF8String |
Constructed Types | SEQUENCE SET CHOICE |
上述的基础类型可以在这里找到详尽的解释。我们可以使用这些来描述我们自己的数据结构:
FooQuestion ::= SEQUENCE {
trackingNumber INTEGER,
question IA5String
}
如上定义了一个名为FooQuestion的数据结构。它是一个SEQUENCE结构,包含了一个INTEGER一个IA5String
一个具体的FooQuestion可以描述为:
myQuestion FooQuestion ::= {
trackingNumber 5,
question "Anybody there?"
}
用ASN.1定义的数据结构实例,可以序列化为二进制的BER、文本类型的JSON、XML等。
Object Identifier
Object Identifier (OID)是一项由ITU和ISO/IEC制定的标准,用来唯一标识对象、概念,或者其它任何具有全球唯一特性的东西。
一个OID表现为用.分隔的一串数字,比如椭圆曲线secp256r1的OID是这样:
1.2.840.10045.3.1.7
其每个数字的含义如下:
iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 7
OID是全局统一分配的,全部的OID可以看做一棵多叉树,每一个有效的OID表现为树上的一个节点。当前所有的OID可以在这里找到。
OID是ASN.1的基本类型。
BER & DER
Basic Encoding Rules (BER)是一种自描述的ASN.1数据结构的二进制编码格式。每一个编码后的BER数据依次由数据类型标识(Type identifier),长度描述(Length description), 实际数据(actual Value)排列而成,即BER是一种二进制TLV编码。TLV编码的一个好处,是数据的解析者不需要读取完整的数据,仅从一个不完整的数据流就可以开始解析。
Distinguished Encoding Rules (DER)是BER的子集,主要是消除了BER的一些不确定性的编码规则,比如在BER中Boolean类型true的value字节,可以为任何小于255大于0的整数,而在DER中,value字节只能为255。DER的这种确定性,保证了一个ASN.1数据结构,在编码为为DER后,只会有一种正确的结果。这使得DER更适合用在数字签名领域,比如X.509中广泛使用了DER。
关于各种ASN.1数据类型是如何被编码为DER,可以在这里找到详尽的解释。
如果有DER数据需要解析查看内容,这里有一个很方便的在线工具。
用DER来编码ASN.1小节中自定义的myQuestion如下:
0x30 0x13 0x02 0x01 0x05 0x16 0x0e 0x41 0x6e 0x79 0x62 0x6f 064 0x79 0x20 0x74 0x68 0x65 0x72 0x65 0x3f
--- --- --- --- --- --- --- --------------------------------------------------------------------
^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | |
| | | INTEGER | IA5STRING |
| | | LEN=1 | TAG | |
| | | | | |
| | INTEGER INTEGER IA5STRING IA5STRING
| | TAG VALUE(5) LEN=14 VALUE("Anybody there?")
| |
| | ----------------------------------------------------------------------------------------------
| | ^
| SEQUENCE LEN=19 |
| |
SEQUENCE TAG SEQUENCE VALUE
PEM
DER格式是ASN.1数据的二进制编码,计算机处理方便,但不利于人类处理,比如不方便直接在邮件正文中粘贴发送。PEM是DER格式的BASE64编码。除此之外,PEM在DER的BASE64前后各增加了一行,用来标识数据内容。示例如下:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMYfnvWtC8Id5bPKae5yXSxQTt
+Zpul6AnnZWfI2TtIarvjHBFUtXRo96y7hoL4VWOPKGCsRqMFDkrbeUjRrx8iL91
4/srnyf6sh9c8Zk04xEOpK1ypvBz+Ks4uZObtjnnitf0NBGdjMKxveTq+VE7BWUI
yQjtQ8mbDOsiLLvh7wIDAQAB
-----END PUBLIC KEY-----
X.509
X.509是一项描述公钥证书结构的标准,广泛使用在HTTPS协议中,定义在RFC 3280
X.509使用ASN.1来描述公钥证书的结构,通常编码为DER格式,也可以进一步BASE64编码为可打印的PEM格式。V3版本的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 }
SubjectPublicKeyInfo
如上一节所示,SubjectPublicKeyInfo是公钥证书格式X.509的组成部分。SubjectPublicKeyInfo结构使用ASN.1描述,其中使用了椭圆曲线公私钥加密算法的SubjectPublicKeyInfo结构定义在RFC 5480
其结构如下:
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING
}
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}
可以看到AlgorithmIdentifier也是一个SEQUENCE,其parameters部分取决于algorithm的具体取值。
对不限制的ECC公钥使用算法的场景,algorithm取值:
1.2.840.10045.2.1
即: iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1
在该种类场景下,parameters的定义如下:
ECParameters ::= CHOICE {
namedCurve OBJECT IDENTIFIER
}
即parameters指定了ECC公钥所使用的椭圆曲线。其可选的值有:
secp192r1 OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 1 }
sect163k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 1 }
sect163r2 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 15 }
secp224r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 33 }
sect233k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 26 }
sect233r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 27 }
secp256r1 OBJECT IDENTIFIER ::= {
iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) prime(1) 7 }
sect283k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 16 }
sect283r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 17 }
secp384r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 34 }
sect409k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 36 }
sect409r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 37 }
secp521r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 35 }
sect571k1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 38 }
sect571r1 OBJECT IDENTIFIER ::= {
iso(1) identified-organization(3) certicom(132) curve(0) 39 }
algorithm确定后,再来看下subjectPublicKey,对ECC公钥来讲,subjectPublicKey就是ECPoint:
ECPoint ::= OCTET STRING
是长度为65字节的OCTET STRING,其中第一个字节代表ECPoint是否经过压缩,如果为0x04,代表没有压缩。剩下的64个字节,前32个字节,表示ECPoint的X坐标,后32个字节表示ECPoint的Y坐标。
OCTET STRING类型的ECPoint在转换为BIT STRING类型的subjectPublicKey时,按照大端字节序转换。
ECC Public Key Example
我们以一个DER编码的ECC公钥为例,详细剖析一下X.509 ECC公钥的格式。公钥内容如下:
0x30 0x59 0x30 0x13 0x06 0x07
0x2a 0x86 0x48 0xce 0x3d 0x02
0x01 0x06 0x08 0x2a 0x86 0x48
0xce 0x3d 0x03 0x01 0x07 0x03
0x42 0x00 0x04 0x13 0x32 0x8e
0x0c 0x11 0x8a 0x70 0x1a 0x9e
0x18 0xa3 0xa9 0xa5 0x65 0xd8
0x41 0x68 0xce 0x2f 0x5b 0x11
0x94 0x57 0xec 0xe3 0x67 0x76
0x4a 0x3f 0xb9 0xec 0xd1 0x15
0xd0 0xf9 0x56 0x8b 0x15 0xe6
0x06 0x2d 0x72 0xa9 0x45 0x56
0x99 0xb0 0x9b 0xb5 0x30 0x90
0x8d 0x2e 0x31 0x0e 0x95 0x68
0xcc 0xcc 0x19 0x5c 0x65 0x53
0xba
通过前面的介绍,我们已经知道这是一个ASN.1格式的SubjectPublicKeyInfo的DER编码,是一个TLV类型的二进制数据。现在我们逐层解析下:
0x30 (SEQUENCE TAG: SubjectPublicKeyInfo) 0x59 (SEQUENCE LEN=89)
0x30 (SEQUENCE TAG: AlgorithmIdentifier) 0x13 (SEQUENCE LEN=19)
0x06 (OID TAG: Algorithm) 0x07 (OID LEN=7)
0x2a 0x86 0x48 0xce 0x3d 0x02 0x01 (OID VALUE="1.2.840.10045.2.1": ecPublicKey/Unrestricted Algorithm Identifier)
0x06 (OID TAG: ECParameters:NamedCurve) 0x08 (OID LEN=8)
0x2a 0x86 0x48 0xce 0x3d 0x03 0x01 0x07 (OID VALUE="1.2.840.10045.3.1.7": Secp256r1/prime256v1)
0x03 (BIT STRING TAG: SubjectPublicKey:ECPoint) 0x42 (BIT STRING LEN=66) 0x00 (填充bit数量为0)
0x04 (未压缩的ECPoint)
0x13 0x32 0x8e 0x0c 0x11 0x8a 0x70 0x1a 0x9e 0x18 0xa3 0xa9 0xa5 0x65 0xd8 0x41 0x68 0xce 0x2f 0x5b 0x11 0x94 0x57 0xec 0xe3 0x67 0x76 0x4a 0x3f 0xb9 0xec 0xd1 (ECPoint:X)
0x15 0xd0 0xf9 0x56 0x8b 0x15 0xe6 0x06 0x2d 0x72 0xa9 0x45 0x56 0x99 0xb0 0x9b 0xb5 0x30 0x90 0x8d 0x2e 0x31 0x0e 0x95 0x68 0xcc 0xcc 0x19 0x5c 0x65 0x53 0xba (ECPoint:Y)
Java Code
本节给出使用使用Java来生成ECC公私钥、编码解码ECC公私钥、使用ECC进行签名验签、加密解密相关的示例代码供参考。在Java中使用ECC算法有以下几点需要注意:
- JDK1.7开始内置了ECC公私钥生成、签名验签,但没有实现加密解密,因此需要使用BouncyCastle来做Security Provider;
- 在Java中使用高级别的加解密算法,比如AES使用256bit密钥、ECC使用Secp256r1等需要更新JRE的security policy文件,否则会报类似“Illegal key size or default parameters”这样的错误。具体怎样更换policy文件,可以参考这里
- 实际项目开发过程中,可能发现有传递给Java的公钥不是完整的X.509 SubjectPublicKeyInfo,比如只传递了一个65字节的ECPoint过来,这种情况可以跟对方沟通清楚所使用的Algorithm以及NamedCurve,补全DER数据后,再使用Java Security库解析。
JDK 1.7
依赖:org.bouncycastle:bcprov-jdk15on:1.59
import com.sun.jersey.core.util.Base64;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.binary.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ECCUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(ECCUtils.class);
private static final String PROVIDER = "BC";
private static final byte[] PUB_KEY_TL= new byte[26];
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
try {
KeyPair keyPair = genKeyPair();
PublicKey publicKeyExample = keyPair.getPublic();
System.arraycopy(publicKeyExample.getEncoded(), 0, PUB_KEY_TL, 0, 26);
} catch (Exception e) {
LOGGER.error("无法初始化算法", e);
}
}
public static KeyPair genKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", PROVIDER);
keyPairGenerator.initialize(256, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
public static String encodePublicKey(PublicKey publicKey) {
return StringUtils.newStringUtf8(Base64.encode(publicKey.getEncoded()));
}
public static PublicKey decodePublicKey(String keyStr)
throws NoSuchProviderException, NoSuchAlgorithmException {
byte[] keyBytes = getPubKeyTLV(keyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER);
try {
return keyFactory.generatePublic(keySpec);
} catch (InvalidKeySpecException e) {
LOGGER.error("无效的ECC公钥", e);
return null;
}
}
private static byte[] getPubKeyTLV(String keyStr) {
byte[] keyBytes = Base64.decode(StringUtils.getBytesUtf8(keyStr));
if(keyBytes.length == 65) {
byte[] tlv = new byte[91];
System.arraycopy(PUB_KEY_TL, 0, tlv, 0, 26);
System.arraycopy(keyBytes, 0, tlv, 26, 65);
return tlv;
}
return keyBytes;
}
public static String encodePrivateKey(PrivateKey privateKey) {
return StringUtils.newStringUtf8(Base64.encode(privateKey.getEncoded()));
}
public static PrivateKey decodePrivateKey(String keyStr)
throws NoSuchProviderException, NoSuchAlgorithmException {
byte[] keyBytes = Base64.decode(StringUtils.getBytesUtf8(keyStr));
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER);
try {
return keyFactory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
LOGGER.error("无效的ECC私钥", e);
return null;
}
}
public static byte[] encrypt(byte[] content, PublicKey publicKey)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
Cipher cipher = Cipher.getInstance("ECIES", PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(content);
}
public static byte[] decrypt(byte[] content, PrivateKey privateKey)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
Cipher cipher = Cipher.getInstance("ECIES", PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(content);
}
public static byte[] signature(byte[] content, PrivateKey privateKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initSign(privateKey);
signature.update(content);
return signature.sign();
}
public static boolean verify(byte[] content, byte[] sign, PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException {
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initVerify(publicKey);
try {
signature.update(content);
return signature.verify(sign);
} catch (SignatureException e) {
LOGGER.warn("无效的签名", e);
return false;
}
}
}
总结:密码学相关的标准、协议很多,原理往往需要一些数学基础。想要程序马上work起来可能容易,想要搞清楚原理,需要花些时间才行。
下一篇: 通过微信小程序实现2048小游戏
推荐阅读
-
ECC公钥格式的详细解析(转载)
-
C 语言和加密算法的实现:详细介绍 RSA、AES、ECC 及其他公钥和对称加密算法 (III)
-
ssh工作流程及原理-SSH(Secure Shell Protocol,安全的壳程序协议),它可以通过数据包加密技术将等待传输的数据包加密后再传输到网络上。ssh协议本身提供两个服务器功能:一个是类似telnet的远程连接使用shell的服务器;另一个就是类似ftp服务的sftp-server,提供更安全的ftp服务。 连接加密技术简介 目前常见的网络数据包加密技术通常是通过“非对称密钥系统”来处理的。主要通过两把不一样的公钥与私钥来进行加密与解密的过程。 公钥(public key):提供给远程主机进行数据加密的行为,所有人都可获得你的公钥来将数据加密。 私钥(private key):远程主机使用你的公钥加密的数据,在本地端就能够使用私钥来进行解密。私钥只有自己拥有。 SSH工作过程:在整个通讯过程中,为实现SSH的安全连接,服务端与客户端要经历如下五个阶段: 版本号协商阶段 SSH目前包括SSH1和SSH2两个版本,双方通过版本协商确定使用的版本 密钥和算法协商阶段 SSH支持多种加密算法,双方根据本端和对端支持的算法,协商出最终使用的算法 认证阶段 SSH客户端向服务器端发起认证请求,服务器端对客户端进行认证 会话请求阶段 认证通过后,客户端向服务器端发送会话请求 交互会话阶段 会话请求通过后,服务器端和客户端进行信息的交互 一、版本协商阶段 服务器端打开端口22,等待客户端连接; 客户端向服务器端发起TCP初始连接请求,TCP连接建立后,服务器向客户端发送第一个报文,包括版本标志字符串,格式为“SSH-<主协议版本号>.<次协议版本号>.<软件版本号>”,协议版本号由主版本号和次版本号组成,软件版本号主要是为调试使用。 客户端收到报文后,解析该数据包,如果服务器的协议版本号比自己的低,且客户端能支持服务器端的低版本,就使用服务器端的低版本协议号,否则使用自己的协议版本号。 客户端回应服务器一个报文,包含了客户端决定使用的协议版本号。服务器比较客户端发来的版本号,决定是否能同客户端一起工作。如果协商成功,则进入密钥和算法协商阶段,否则服务器断开TCP连接。 说明:上述报文都是采用明文方式传输。 二、密钥和算法协商阶段 服务器端和客户端分别发送算法协商报文给对端,报文中包含自己支持的公钥算法列表、加密算法列表、MAC(Message Authentication Code,消息验证码)算法列表、压缩算法列表等等。 服务器端和客户端根据对端和本端支持的算法列表得出最终使用的算法。 服务器端和客户端利用DH交换(Diffie-Hellman Exchange)算法、主机密钥对等参数,生成会话密钥和会话ID。 由此,服务器端和客户端就取得了相同的会话密钥和会话ID。对于后续传输的数据,两端都会使用会话密钥进行加密和解密,保证了数据传送的安全。在认证阶段,两端会使用会话用于认证过程。 会话密钥的生成: 客户端需要使用适当的客户端程序来请求连接服务器,服务器将服务器的公钥发送给客户端。(服务器的公钥产生过程:服务器每次启动sshd服务时,该服务会主动去找/etc/ssh/ssh_host*文件,若系统刚装完,由于没有这些公钥文件,因此sshd会主动去计算出这些需要的公钥文件,同时也会计算出服务器自己所需要的私钥文件。) 服务器生成会话ID,并将会话ID发给客户端。 若客户端第一次连接到此服务器,则会将服务器的公钥数据记录到客户端的用户主目录内的~/.ssh/known_hosts。若是已经记录过该服务器的公钥数据,则客户端会去比对此次接收到的与之前的记录是否有差异。客户端生成会话密钥,并用服务器的公钥加密后,发送给服务器。 ****服务器用自己的私钥将收到的数据解密,获得会话密钥。 服务器和客户端都知道了会话密钥,以后的传输都将被会话密钥加密。 三、认证阶段 SSH提供两种认证方法: 基于口令的认证(password认证):客户端向服务器发出password认证请求,将用户名和密码加密后发送给服务器,服务器将该信息解密后得到用户名和密码的明文,与设备上保存的用户名和密码进行比较,并返回认证成功或失败消息。 基于密钥的认证(publickey认证):客户端产生一对公共密钥,将公钥保存到将要登录的服务器上的那个账号的家目录的.ssh/authorized_keys文件中。认证阶段:客户端首先将公钥传给服务器端。服务器端收到公钥后会与本地该账号家目录下的authorized_keys中的公钥进行对比,如果不相同,则认证失败;否则服务端生成一段随机字符串,并先后用客户端公钥和会话密钥对其加密,发送给客户端。客户端收到后将解密后的随机字符串用会话密钥发送给服务器。如果发回的字符串与服务器端之前生成的一样,则认证通过,否则,认证失败。 注:服务器端对客户端进行认证,如果认证失败,则向客户端发送认证失败消息,其中包含可以再次认证的方法列表。客户端从认证方法列表中选取一种认证方法再次进行认证,该过程反复进行。直到认证成功或者认证次数达到上限,服务器关闭连接为止。实例