【网络安全】https与证书原理 | SSL Pinning及其绕过 2025-06-19 网络安全 暂无评论 19 次阅读 #SSL Pinning ##1 HTTPS协议流程 参考: https://segmentfault.com/a/1190000009002353?sort=newest https://zhuanlan.zhihu.com/p/353571366 https://juejin.cn/post/6863295544828444686 HTTPS=HTTP+TLS,其它的协议也类似,如FTPS=FTP+TLS  1) ClientHello - Client 首先发送本地的 TLS 版本、支持的加密算法套件,并且生成一个随机数 R1 。 2)Server Hello - Server 端确认 TLS 版本号。从 Client 端支持的加密套件中选取一个,并生成一个随机数 R2 一起发送给 Client。 - Server 向 Client 发送自己的CA证书(包含公钥、证书签名)。 3)证书校验 - Client 判断证书签名与CA证书是否合法有效 - Client 生成随机数pre-master secret,并使用Server发过来的公钥对pre-master secret进行加密,将加密后的pre-master secret送给Server。这一步结束后,Client 与 Server 就都有 R1、R2、pre-master secret 了,两端便可以使用这 3 个随机数独立生成 对称会话密钥了,避免了对称密钥的传输,同时可以 根据会话密钥生成 6 个密钥(P1~P6) 用作后续身份验证 Client端和Server端,最终都会用相同的算法将pre-master secret(预主密钥)转换成master secret(主密钥),通过主密钥可以生成session key。两者后续的通信交互数据,将通过session key进行加密。  参考:https://www.laoqingcai.com/tls1.2-premasterkey/ 4)Client 握手结束通知 - Client 使用 P1 将之前的握手信息的 hash 值加密并发送给 Server - Client 发送握手结束消息 5)Server 握手结束通知 - Server 计算之前的握手信息的 hash 值,并与 P1 解密客户端发送的握手信息的 hash 对比校验 - 验证通过后,使用 P2 将之前的握手信息的 hash 值加密并发送给 Client 6)Client 开始HTTPS通讯 - Client 计算之前的握手信息的 hash 值,并与 P2 解密 Server 发送的握手信息的 hash 对比校验 - 验证通过后,开始发起 HTTPS 请求。 两者后续的通信交互数据,将通过session key进行加密。所以中间人即使截获数据,也无法解析。 ##2 证书相关 ###证书文件 证书=公钥+(公钥+元信息)的签名  其中的元信息包括: - Subject(主体信息): - Common Name(CN)通用名称 - SAN - Organization - Organization Unit(OU) - Country - State - City - Address - Postal code - Issuer(签发者信息): - Common Name(CN)通用名称 - Organization - Organization Unit(OU) - Country - State - City - Address - Postal code - Validity(有效期): - Not Before(签发日期) - Not After(过期时间) - Signature Algorithm - Serial Number - Version - Extensions(扩展信息):只在证书版本2、3中才有 因此,证书的结构大致如下:  #CA 签名 = 计算摘要 + 对摘要值私钥加密 CA:Certificate Authority,专门用自己的私钥 给别人进行签名的机构 #签发证书的过程 注意,计算签名时,是对整个证书文件计算签名,也就是对【元信息+公钥】计算签名,而不只是对公钥计算签名。  (参考:https://blog.csdn.net/bluishglc/article/details/123617558 ) #证书的验证过程 关键过程:用信任CA库里CA证书(公钥),验证网站的证书文件里的签名 1.在TLS握手的过程中,客户端得到了网站的证书 2.客户端打开证书,查看是哪个CA签名的这个证书 3.在自己信任的CA库中,找相应CA的证书(包含CA的公钥), 4.用CA证书里面的公钥解密网站证书上的签名,取出网站证书的摘要,然后用同样的算法(比如sha256)算出网站证书的摘要,如果摘要和签名中的摘要对的上,说明这个证书是合法的,且没被人篡改过 5.读出里面的CN,对于网站的证书,里面一般包含的是域名 6.检查里面的域名和自己访问网站的域名对不对的上,对的上,就说明这个证书确实是颁发给这个网站的 7.到此为止检查通过 #证书链的验证 参考: https://www.jianshu.com/p/46e48bc517d0 https://www.cnblogs.com/xiaxveliang/p/13183175.html 我们使用End-user Certificates来确保加密传输数据的公钥(public key)不被篡改,而又如何确保end-user certificates的合法性呢? 这个认证过程跟公钥的认证过程类似,首先获取颁布end-user certificates的CA的证书,然后验证end-user certificates的signature。一般来说,root CAs不会直接颁布end-user certificates的,而是授权给多个二级CA,而二级CA又可以授权给多个三级CA,这些中间的CA就是Intermediates CAs,它们才会颁布end-user certificates。 但是Intermediates Certificates的可靠性又如何保证呢?这就是涉及到证书链,Certificate Chain ,链式向上验证证书,直到Root Certificates,如下图:  中间CA的证书怎么获取? 以百度的TLS证书进行举例,百度服务器证书 签发者公钥(中间机构公钥)通过下图中的URI获取:  # 3 SSL Pinning 参考: https://shunix.com/ssl-pinning/ https://zhuanlan.zhihu.com/p/58204817 ##3.1 原理 默认情况下,只要网站证书的Root CA,属于系统信任的Root CA集合(例如,安卓中系统默认信任 /system/etc/security/ 中CA证书对应的CA)。 对于 `www.example.com` ,可能出现以下情况: ##(1)情况A: 某个系统信任的Root CA,授权给了可靠的Intermediate CA 1,Intermediate CA 1给 `www.example.com` 颁发了一个合法的证书1; 同时该Root CA也授权给了不可靠的Intermediate CA 2(不可靠的原因可能是私钥被泄露),Intermediate CA 2给 `www.example.com`颁发了一个证书2。 这时候我们希望只信任证书1而不信任证书2,否则一些中间人拿到了证书2,就可以伪装成合法的`www.example.com`。 这通过修改信任CA集合是较难实现的,因为两个证书的根信任锚是相同的Root CA。当然,可以从信任集中删除Root CA,再添加Intermediate CA 1而不添加Intermediate CA 2。但这意味着我们需要移除Root CA。通常,一个Root CA会作为成千上万个证书的根信任锚,移除Root CA可能引发过大的影响。 (2)情况B: 系统的可信CA集合被篡改。例如,安卓系统在被Root的情况下,用户可以修改系统信任证书(方法例如:https://github.com/doug-leith/cydia )。 这种情况下,app可能需要只信任特定的某个(某些)证书。 ##原理: 可以采用证书固定。只有当网站的证书链中,至少有一个节点的证书全部内容/证书公钥,跟客户端预埋的证书的内容相匹配,我们的客户端才信任此证书链。 ##证书固定 与 限制可信CA 的关系 如果把某个Root CA的证书固定起来,那就相当于设置该Root CA为唯一可信的Root CA。 被固定的证书可以是(一般是)某个中间CA的证书。这样,不再是所有以trusted Root CA为根的证书链都仍旧可信了。只有子节点包含该中间CA的证书链才可信。 被固定的证书的Root CA可以不在系统trusted Root CA集合中。 ##3.2 实现方案 具体实现技术上,SSL Pinning可以分为Certificate Pinning(证书固定)和Public Key Pinning(公钥固定) ##3.2.1 证书固定 把证书文件打包进安装包,将app设置为仅接受指定的内置证书,而不接受操作系统内置的CA根证书对应的任何证书。 ##3.2.2 公钥固定 提取证书中的公钥并内置到App中,通过与服务器对比公钥值,来验证连接的合法性。我们在申请证书时,公钥在证书的续期前后可以保持不变,所以可以解决证书有效期问题。 ##3.3 实例 ##3.3.1 证书固定实例:基于TrustManagerFactory ``` // kotlin语法 // 加载证书文件 val cf: CertificateFactory = CertificateFactory.getInstance("X.509") val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt")) // 使用CertificateFactory生成一个X509Certificate的实例 val ca: X509Certificate = caInput.use { cf.generateCertificate(it) as X509Certificate } System.out.println("ca=" + ca.subjectDN) // 创建一个KeyStore实例,并把前边的X509Certificate实例加进去,并起一个别名"ca" val keyStoreType = KeyStore.getDefaultType() val keyStore = KeyStore.getInstance(keyStoreType).apply { load(null, null) setCertificateEntry("ca", ca) } // 创建一个TrustManagerFactory实例,并且使用前边的KeyStore实例进行初始化 val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm() val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply { init(keyStore) } // 创建一个SSLContext实例,并且使用前面的TrustManagerFactory实例的trustManagers进行初始化 val context: SSLContext = SSLContext.getInstance("TLS").apply { init(null, tmf.trustManagers, null) } // 创建HttpsURLConnection实例urlConnection val url = URL("https://certs.cac.washington.edu/CAtest/") val urlConnection = url.openConnection() as HttpsURLConnection // 将SSLContext实例context的socketFactory属性,赋值给urlConnection urlConnection.sslSocketFactory = context.socketFactory val inputStream: InputStream = urlConnection.inputStream copyInputStreamToOutputStream(inputStream, System.out) ``` ##3.3.2 证书固定实例:基于NSC配置文件 需要在Manifest文件的`android:networkSecurityConfig`属性加上对应的配置内容,示例如下: ``` example.com example.com 7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y= fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE= ``` 关于NSC的详细内容,可以参考论文: [USENIX Sec’21] Why Eve and Mallory Still Love Android: Revisiting TLS (In)Security in Android Applications 或者直接参考Google的官网文档: https://developer.android.com/training/articles/security-config 计划后续写一篇博客详细介绍Google Android的NSC。 # 4 安卓中的SSL Pinning 参考:http://hanpfei.github.io/2018/03/20/android_cert_mgr_and_verify/ SSL Pinning机制中,客户端将特定域名的证书与特定的签发者绑定。即,对某个域名,客户端只承认特定CA为该域名签发的证书,而不承认其它 CA 为该域名签发的证书。 ##4.1 Android 的根证书管理 AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates 目录下,而在 Android 系统中,则存放在 /system/etc/security/ 目录下:  cacerts_google 目录下的根证书,主要用于 system/update_engine、external/libbrillo 和 system/core/crash_reporter 等模块 cacerts 目录下的根证书则用于所有的应用。cacerts 目录下的根证书,即 Android 系统的根证书库,像下面这样:  它们都是 PEM 格式的 X.509 证书。 Android 系统通过 SystemCertificateSource、DirectoryCertificateSource 和 CertificateSource 等类管理系统根证书库。 - CertificateSource定义了可以对根证书库执行的操作,主要是对根证书的获取和查找: 位于frameworks/base/core/java/android/security/net/config/CertificateSource.java - DirectoryCertificateSource 类提供证书的创建、获取和查找操作: 位于frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java 获取根证书库的 getCertificates() 操作在第一次被调用时,遍历文件系统,并加载系统所有的根证书文件,并缓存起来,以备后面访问。 根证书的查找操作,主要依据证书文件的文件名进行,证书文件被要求以 [SubjectName 的哈希值].[Index] 的形式命名。 - SystemCertificateSource 类定义了系统根证书库的路径,以及无效一个根证书的机制: 位于frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java Android 系统的根证书位于 /system/etc/security/cacerts/ 目录下。用户可以通过将特定根证书复制到用户配置目录的 cacerts-removed 目录下来无效一个根证书。 ##4.2 证书链合法性验证 OpenSSLSocketImpl.startHandshake() 通过 NativeCrypto 类的 SSL_do_handshake() 方法执行握手操作: (NativeCrypto 位于external/conscrypt/src/main/java/org/conscrypt/NativeCrypto.java) SSL_do_handshake() 方法的第三参数是一个接口:SSLHandshakeCallbacks  SSLHandshakeCallbacks是NativeCrypto 类定义的接口,其中包含一组回调函数;这组回调函数,是SSL_do_handshake() 的参数,在SSL_do_handshake() 中被传入native层 SSLHandshakeCallbacks 中的方法之一是verifyCertificateChain(): ``` /** * A collection of callbacks from the native OpenSSL code that are * related to the SSL handshake initiated by SSL_do_handshake. */ public interface SSLHandshakeCallbacks { /** * Verify that we trust the certificate chain is trusted. * * @param sslSessionNativePtr pointer to a reference of the SSL_SESSION * @param certificateChainRefs chain of X.509 certificate references * @param authMethod auth algorithm name * * @throws CertificateException if the certificate is untrusted */ public void verifyCertificateChain(long sslSessionNativePtr, long[] certificateChainRefs, String authMethod) throws CertificateException; ``` verifyCertificateChain()的参数: - 指向一个sslSession的指针 - X.509 证书链 - 认证算法名称 SSLHandshakeCallbacks中的回调方法的实现在 OpenSSLSocketImpl 。 OpenSSLSocketImpl中,verifyCertificateChain()的实现如下: ``` @SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks @Override public void verifyCertificateChain(long sslSessionNativePtr, long[] certRefs, String authMethod) throws CertificateException { try { X509TrustManager x509tm = sslParameters.getX509TrustManager(); if (x509tm == null) { throw new CertificateException("No X.509 TrustManager"); } if (certRefs == null || certRefs.length == 0) { throw new SSLException("Peer sent no certificate"); } OpenSSLX509Certificate[] peerCertChain = new OpenSSLX509Certificate[certRefs.length]; for (int i = 0; i < certRefs.length; i++) { peerCertChain[i] = new OpenSSLX509Certificate(certRefs[i]); } // Used for verifyCertificateChain callback handshakeSession = new OpenSSLSessionImpl(sslSessionNativePtr, null, peerCertChain, getHostnameOrIP(), getPort(), null); boolean client = sslParameters.getUseClientMode(); if (client) { Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this); if (sslParameters.isCTVerificationEnabled(getHostname())) { byte[] tlsData = NativeCrypto.SSL_get_signed_cert_timestamp_list( sslNativePointer); byte[] ocspData = NativeCrypto.SSL_get_ocsp_response(sslNativePointer); CTVerifier ctVerifier = sslParameters.getCTVerifier(); CTVerificationResult result = ctVerifier.verifySignedCertificateTimestamps(peerCertChain, tlsData, ocspData); if (result.getValidSCTs().size() == 0) { throw new CertificateException("No valid SCT found"); } } } else { String authType = peerCertChain[0].getPublicKey().getAlgorithm(); Platform.checkClientTrusted(x509tm, peerCertChain, authType, this); } } catch (CertificateException e) { throw e; } catch (Exception e) { throw new CertificateException(e); } finally { // Clear this before notifying handshake completed listeners handshakeSession = null; } } ``` 这里面,verifyCertificateChain() 从 OpenSSLSocketImpl的 sslParameters 获得 X509TrustManager: ``` X509TrustManager x509tm = sslParameters.getX509TrustManager(); ``` 然后在 Platform.checkServerTrusted() 中执行服务端证书合法有效性的检查: ``` Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this); ``` Platform.checkServerTrusted在com.android.org.conscrypt.Platform类(external/conscrypt/src/compat/java/org/conscrypt/Platform.java): ``` public static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain, String authType, OpenSSLSocketImpl socket) throws CertificateException { if (!checkTrusted("checkServerTrusted", tm, chain, authType, Socket.class, socket) && !checkTrusted("checkServerTrusted", tm, chain, authType, String.class, socket.getHandshakeSession().getPeerHost())) { tm.checkServerTrusted(chain, authType); } } ``` 可以看到,Platform.checkServerTrusted()会调用X509TrustManager.checkServerTrusted()来完成检查。 其中的X509TrustManager实例来源于OpenSSLSocketImpl 的sslParameters,如前文所述: ``` X509TrustManager x509tm = sslParameters.getX509TrustManager(); ``` 那OpenSSLSocketImpl 的 sslParameters 又来自于哪里呢?来源于构造函数,例如: ``` protected OpenSSLSocketImpl(SSLParametersImpl sslParameters) throws IOException { this.socket = this; this.peerHostname = null; this.peerPort = -1; this.autoClose = false; this.sslParameters = sslParameters; } ``` 而OpenSSLSocketFactoryImpl类会实例化OpenSSLSocketImpl: ``` package org.conscrypt; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; public class OpenSSLSocketFactoryImpl extends javax.net.ssl.SSLSocketFactory { private final SSLParametersImpl sslParameters; private final IOException instantiationException; ………… @Override public Socket createSocket() throws IOException { if (instantiationException != null) { throw instantiationException; } return new OpenSSLSocketImpl((SSLParametersImpl) sslParameters.clone()); } @Override public Socket createSocket(String hostname, int port) throws IOException, UnknownHostException { return new OpenSSLSocketImpl(hostname, port, (SSLParametersImpl) sslParameters.clone()); } @Override public Socket createSocket(String hostname, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return new OpenSSLSocketImpl(hostname, port, localHost, localPort, (SSLParametersImpl) sslParameters.clone()); } @Override public Socket createSocket(InetAddress address, int port) throws IOException { return new OpenSSLSocketImpl(address, port, (SSLParametersImpl) sslParameters.clone()); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return new OpenSSLSocketImpl(address, port, localAddress, localPort, (SSLParametersImpl) sslParameters.clone()); } } ``` 后面的细节暂时略过不看。 ##总结: OpenSSLSocketImpl.startHandshake() 和 NativeCrypto.SSL_do_handshake() 执行完整的 SSL/TLS 握手过程。 证书合法性验证是 SSL/TLS 握手的一个重要步骤。该过程通过 native层调用Java 层的回调方法 SSLHandshakeCallbacks.verifyCertificateChain() 来完成。 回调方法的实现在OpenSSLSocketImpl。 OpenSSLSocketImpl.verifyCertificateChain()调用Platform.checkServerTrusted(),调用RootTrustManager.checkServerTrusted() ,调用NetworkSecurityTrustManager.checkServerTrusted() ,将真正根据系统根证书库执行证书合法性验证的 TrustManagerImpl 和 SSL/TLS 握手过程结合起来。 > OpenSSLSocketFactoryImpl 将 OpenSSLSocketImpl 和 SSLParametersImpl 粘起来。 > SSLParametersImpl 将 OpenSSLSocketImpl 和 RootTrustManager 粘起来。 > NetworkSecurityConfig 将 RootTrustManager 和 NetworkSecurityTrustManager 粘起来。 > NetworkSecurityConfig、NetworkSecurityTrustManager 和 TrustedCertificateStoreAdapter 将 TrustManagerImpl 和管理系统根证书库的SystemCertificateSource 粘起来。 TrustManagerImpl 是证书合法性验证的核心,它会查找系统根证书库,并验证服务端证书的合法性做。 这个过程的调用栈如下: ``` com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted() android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted() android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted() android.security.net.config.RootTrustManager.checkServerTrusted() com.android.org.conscrypt.Platform.checkServerTrusted() com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain() com.android.org.conscrypt.NativeCrypto.SSL_do_handshake() com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake() com.android.okhttp.Connection.connectTls() ``` ##4.3 自定义证书(SSL Pinning) 在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。 要想定制 OpenSSLSocketImpl 的证书验证过程,必然要改变 SSLParametersImpl;要改变 OpenSSLSocketImpl 的 SSLParametersImpl,则必然需要修改 SSLSocketFactory。修改 SSLSocketFactory 常常是一个不错的方法。 两种实现手段: (1)自己实现 X509TrustManager 像下面这样: ``` private final class HelloX509TrustManager implements X509TrustManager { private X509TrustManager mSystemDefaultTrustManager; private X509Certificate mCertificate; private HelloX509TrustManager() { mCertificate = loadRootCertificate(); mSystemDefaultTrustManager = systemDefaultTrustManager(); } private X509Certificate loadRootCertificate() { String certName = "netease.crt"; X509Certificate certificate = null; InputStream certInput = null; try { certInput = new BufferedInputStream(MainActivity.this.getAssets().open(certName)); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); certificate = (X509Certificate) certificateFactory.generateCertPath(certInput).getCertificates().get(0); } catch (IOException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } finally { if (certInput != null) { try { certInput.close(); } catch (IOException e) { } } } return certificate; } private X509TrustManager systemDefaultTrustManager() { try { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } return (X509TrustManager) trustManagers[0]; } catch (GeneralSecurityException e) { throw new AssertionError(); // The system has no TLS. Just give up. } } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { mSystemDefaultTrustManager.checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { for (X509Certificate certificate : chain) { try { certificate.verify(mCertificate.getPublicKey()); return; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } } mSystemDefaultTrustManager.checkServerTrusted(chain, authType); } @Override public X509Certificate[] getAcceptedIssuers() { return mSystemDefaultTrustManager.getAcceptedIssuers(); } } ``` (2)仅修改 X509TrustManager 所用的根证书库 ``` private TrustManager[] createX509TrustManager() { CertificateFactory cf = null; InputStream in = null; TrustManager[] trustManagers = null try { cf = CertificateFactory.getInstance("X.509"); in = getAssets().open("ca.crt"); Certificate ca = cf.generateCertificate(in); KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(null, null); keystore.setCertificateEntry("ca", ca); String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keystore); trustManagers = tmf.getTrustManagers(); } catch (CertificateException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return trustManagers; } ``` ##4.4 双向认证 服务端也可能校验客户端的证书(来确保客户端是合法的客户端),这种情况下需要把客户端预存的证书导入中间人抓包工具中。 可以参考: https://www.anquanke.com/post/id/272672 原文链接:https://blog.csdn.net/qq_39441603/article/details/126183031 标签: ssl, https 本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。