
1. 项目概述为什么我们需要一个合规的SM9 Python实现最近在做一个金融领域的项目对接方明确要求使用国密算法SM9进行身份认证和密钥协商并且必须符合GM/T 0044-2016标准最终要通过商用密码应用安全性评估。我一开始想得很简单找个开源的Python库不就行了结果一搜发现市面上能直接拿来商用的、完全合规的SM9实现几乎没有。要么是功能不全只实现了签名验签要么是接口混乱不符合标准规范最要命的是很多实现的安全性未经严格审计在随机数生成、抗侧信道攻击等方面存在隐患这要是用在生产环境评估肯定过不了。这就是我动手搞这个项目的初衷。我需要一个从底层数学运算到上层应用接口都清晰、透明、可审计的SM9 Python实现。它不仅要能用还要用得明白、用得放心能经得起密码测评专家的审视。整个实现过程实际上是一次对SM9标准、椭圆曲线密码学以及密码工程实践的深度梳理。如果你也在为合规的国密算法集成而头疼或者想深入理解SM9的每一个细节那么这篇记录或许能给你提供一个完整的参考路径。2. 核心需求与标准解读GM/T 0044-2016到底要求什么在动手写代码之前我们必须先把标准吃透。GM/T 0044-2016《SM9标识密码算法》这份文档就是我们的“宪法”。它不仅仅规定了算法流程更隐含了对安全性、一致性和互操作性的严苛要求。2.1 SM9算法核心思想与组件SM9属于标识密码算法IBC。和传统的公钥密码体系如RSA、SM2不同用户的公钥直接由其标识符如邮箱、身份证号通过一个公开的算法推导而来私钥则由一个可信的密钥生成中心KGC根据主私钥和该标识符生成。这省去了复杂的公钥证书管理和分发环节。SM9标准主要包含五个部分数字签名算法用于对消息进行签名和验证。密钥封装机制KEM用于封装一个对称密钥以便安全传输。公钥加密算法用于直接加密数据。密钥协商协议用于双方协商出一个共享的会话密钥。密钥交换协议一个更复杂的、包含双向认证的密钥协商过程。我们的实现需要覆盖这五大功能。标准中定义了多组系统参数包括不同安全等级的椭圆曲线参数如256位、512位。对于大多数商用场景使用256-bit的曲线对应BN256或SM9曲线已经足够。2.2 商用密码应用安全性评估的关键点商用密码应用安全性评估简称“密评”关注的是算法在具体应用中的正确、有效和安全使用。对于我们的SM9实现密评会重点考察以下几个方面算法的正确性这是底线。你的签名必须能验过加密必须能解密密钥协商结果双方必须一致。这需要大量的、覆盖边界条件的测试用例。随机数的安全性密码算法的生命线。密钥生成、签名中的随机数k、加密中的随机数r都必须使用密码学安全的随机数生成器CSPRNG。在Python中这意味着必须使用secrets模块或操作系统的/dev/urandomLinux或CryptGenRandomWindows接口绝对禁止使用random模块。抗侧信道攻击虽然Python是高级语言但一些基础操作仍需注意。例如在实现标量乘法k * P时简单的双倍-相加算法执行时间与标量k的汉明重量相关可能泄露信息。虽然对于大多数应用场景这不是首要威胁但一个追求合规的实现应考虑使用常数时间的算法如蒙哥马利阶梯法。密钥管理私钥无论是主私钥还是用户私钥在内存中的存储、使用后的清零、以及最终的销毁都应有明确的规范。Python的垃圾回收机制并不保证立即覆盖内存对于极度敏感的主私钥可能需要借助ctypes等工具进行手动内存管理。标准的符合性所有输入输出格式、编码规则如签名值(r, s)的拼接顺序、密文的ASN.1或TLV结构、哈希函数的使用SM3都必须与标准文档一字不差地对应。任何细微的偏差都会导致与其它合规产品的互操作失败。注意很多开源实现止步于“功能可用”但在随机数、常数时间、内存安全这些“安全性工程”细节上存在缺陷。我们的实现必须从一开始就将这些作为核心设计原则。3. 基础架构与数学库选型要实现SM9我们首先需要一个强大的数学基础库用来处理定义在有限域F_q和扩域F_{q^k}上的椭圆曲线运算特别是双线性对Pairing的计算。这是SM9算法的数学核心。3.1 核心数学运算与库的选择SM9使用的曲线是BN曲线Barreto-Naehrig Curve它具有良好的性质能高效地计算双线性对e: G1 × G2 - GT。在Python中我们有几种选择从头实现不推荐实现一个高效、安全的椭圆曲线库和双线性对库是极其复杂的工程涉及大量的数论和优化技巧极易引入错误和安全漏洞。使用pyca/cryptography等通用库这些库主要提供对称加密和常见非对称算法RSA ECC但通常不直接支持双线性对运算和SM9所需的特定曲线。使用专门的密码学库如Charm-Crypto、PBC库的Python绑定。它们功能强大但可能过于庞大依赖复杂且不一定完全符合GM/T标准的具体参数。使用轻量级专用库例如python-paillier、petlib但同样需要检查对BN曲线的支持。经过调研和测试我选择了ate-pairing库的一个分支或bn256的Python实现作为底层核心。它们相对轻量且专注于BN256曲线上的双线性对运算。我们的策略是以此为基础严格按照GM/T 0044-2016标准文档封装实现SM9所需的全部数学操作。# 示例初始化SM9曲线参数基于一个假设的bn256库 # 注意以下代码仅为示意流程具体API取决于所选基础库 try: from bn256 import curve_order, G1, G2, pairing # 定义SM9用到的群和阶 q curve_order # 曲线阶一个大素数 P1 G1.generator() # G1群的生成元 P2 G2.generator() # G2群的生成元 print(fSM9曲线阶 (q): {hex(q)}) except ImportError: print(请先安装bn256库: pip install bn256) # 在实际项目中这里应提供更详细的安装指引或备选方案3.2 项目结构与模块设计一个清晰的项目结构有助于代码维护和后续的审计。我的项目结构如下sm9_impl/ ├── sm9/ │ ├── __init__.py │ ├── params.py # 定义系统参数、曲线常量、预计算值 │ ├── math_utils.py # 有限域运算、哈希到曲线HashToPoint等底层数学工具 │ ├── core.py # 核心算法类MasterKey, PrivateKey, 签名/加密/密钥协商等 │ ├── kem.py # 密钥封装机制实现 │ ├── key_exchange.py # 密钥交换协议实现 │ └── utils.py # 辅助函数随机数生成、编码解码、错误处理 ├── tests/ # 测试目录 │ ├── test_core.py │ ├── test_kem.py │ ├── test_vectors/ # 存放标准测试向量文件 │ └── ... ├── examples/ # 使用示例 │ ├── signature_demo.py │ └── encryption_demo.py └── requirements.txt在params.py中我们需要严格按照标准附录定义系统参数包括有限域模数q、椭圆曲线方程系数a, b、F_{q^12}扩域的构造参数、G1和G2的生成元坐标等。这些值是算法正确性的基石必须保证零误差。4. 核心算法实现细节与避坑指南有了数学基础和项目框架我们就可以开始实现SM9的各个功能模块了。这里我挑几个最容易出错的“坑点”详细说明。4.1 密钥生成中心KGC与用户密钥生成KGC持有主密钥对主公钥Ppub-s用于签名或Ppub-e用于加密/密钥协商和主私钥ks或ke。用户私钥由KGC使用主私钥和用户ID生成。关键实现点用户ID的编码标准规定ID必须是字节串并且在使用前要经过一个特定的编码函数通常包含ID长度和ID本身。这个编码必须完全按照标准来否则生成的公钥推导和私钥生成都会出错。哈希到曲线HashToPoint这是将任意字节串编码后的ID映射到椭圆曲线群G1或G2上的确定性的、不可逆的函数。标准附录详细描述了H1(ID, N)和H2(ID, N)函数它们输出一个整数再通过MapToPoint算法得到点。务必找到或实现一个经过验证的MapToPoint函数这是很多开源库的薄弱环节。主私钥的安全存储在代码中主私钥是一个大整数。在内存中我们应该尽量缩短其以明文形式存在的时间。使用后应立即用0填充该内存区域在Python中可以创建一个等长的字节数组来覆盖原变量。# 示例用户签名私钥生成核心步骤简化版 import hashlib from sm9.params import N, ks, P1 from sm9.math_utils import hash_to_point_g1, mod_inv def generate_signature_private_key(user_id: bytes) - tuple: 生成用户签名私钥 # 1. 编码用户ID # 标准格式: 0x01 || ID_LEN (2字节) || ID || hid (1字节 签名用0x01) import struct hid 0x01 encoded_id b\x01 struct.pack(H, len(user_id)) user_id bytes([hid]) # 2. 计算哈希并映射到G1群上的点 (H1结果是一个整数需要MapToPoint) # 这里省略了复杂的H1和MapToPoint实现细节 h1_int _hash_id_to_int(encoded_id, N) # 伪函数返回[1, N-1]的整数 # 假设我们有一个将整数映射到G1点的函数 # 注意标准中的H1输出需要模N且不能为0或N if h1_int 0 or h1_int N: raise ValueError(H1 output is invalid) # 这里需要一个真正的 MapToPoint 实现将h1_int映射为G1点G G map_to_point_g1(h1_int) # 这是一个需要精心实现的函数 # 3. 计算用户私钥 ds (1 ks)^-1 * G (在G1群上做标量乘法) # 注意运算是模曲线阶N t (1 ks) % N if t 0: raise ValueError(Master private key ks leads to t0, invalid) t_inv mod_inv(t, N) ds t_inv * G # 椭圆曲线标量乘法 return ds # ds是一个椭圆曲线点即用户的签名私钥 def _hash_id_to_int(data: bytes, modulus: int) - int: 模拟H1函数将ID哈希为整数。实际实现需严格遵循GM/T标准。 # 使用SM3哈希并迭代直到结果在[1, modulus-1]内 # 这是一个简化示例真实H1更复杂 from hashlib import sm3 # 假设有SM3实现 hash_val int.from_bytes(sm3(data).digest(), big) count 0 while hash_val 0 or hash_val modulus: data b\x00 data # 或按标准追加计数器 hash_val int.from_bytes(sm3(data).digest(), big) count 1 if count 100: # 防止意外死循环 raise RuntimeError(Failed to generate valid H1 output) return hash_val % modulus实操心得MapToPoint的实现是第一个大坑。我最初尝试自己根据论文实现但总与官方测试向量对不上。后来我参考了Go语言国密库中的实现并进行了逐行翻译和测试才最终搞定。建议直接寻找一个经过验证的、可读性好的开源实现作为参考。4.2 数字签名与验证的实现签名过程需要生成一个随机数k验证过程则需要计算双线性对。关键实现点随机数k必须来自secrets.randbelow(N)或等效的CSPRNG。k的范围是[1, N-1]。签名值(r, s)的拼接标准规定r和s都是大整数输出签名时通常需要转换为固定长度的字节串如32字节然后拼接。字节序大端序/小端序必须明确并保持一致。通常国密标准采用大端序Big-Endian。双线性对的计算验证公式e(P1, s * P2) e(r * ds G, Ppub-s)涉及两次对运算。确保你使用的配对库函数pairing(A, B)的参数A和B在正确的群上A在G1B在G2。计算出的GT群元素需要比较是否相等通常是比较其坐标的规范化表示。# 示例签名验证核心逻辑简化版 from sm9.math_utils import pairing def verify_signature(master_pub_s, user_id, message, signature) - bool: 验证SM9签名 # 假设 signature r_bytes || s_bytes r int.from_bytes(signature[:32], big) s int.from_bytes(signature[32:], big) # 假设 s_P2 是签名中的s * P2 (一个G2点)实际中签名只传s需要计算s*P2 s_P2 s * P2 # 在G2群上的标量乘法 # 1. 根据用户ID推导其公钥点 (在G2群上) # 类似私钥生成但使用主公钥和哈希到G2的点 # 推导出的公钥点记为 Q_id (在G2群) # 2. 计算消息的哈希并映射到G1群上的点记为 H # 使用标准定义的H2函数和MapToPoint # 3. 计算双线性对进行验证 # 左式: e(P1, s_P2) left pairing(P1, s_P2) # 右式: e(r * H ds, master_pub_s) ? 这里ds是签名者的私钥点验证者不知道。 # 正确的验证公式是: e(r * H Q_id, master_pub_s) left # 其中 Q_id 是用户公钥点 (在G2) master_pub_s 是主公钥点 (在G2) # 注意标准中的公式是 e(P1, S) e(g, r * H Q_id)^ks 经过推导可得到上述形式。 # 更准确的表述 # g pairing(P1, Ppub-s) # 这是一个预计算的GT元素 # 验证 pairing(P1, S) g^r * pairing(Q_id, Ppub-s) # 这里涉及GT群上的指数运算需要根据所选库的API调整。 # 由于配对计算和公式推导较复杂此处省略具体代码。 # 核心是严格按照标准文档第6.2节的验证步骤实现。 pass避坑指南验证环节最容易出错的地方是群元素的混淆。SM9中签名私钥ds在G1验证公钥Q_id在G2签名S在G2而用于计算H的消息哈希映射到G1。务必画一张群与元素的对应关系图写代码时时刻对照避免把G1的点送给需要G2点的函数。4.3 密钥封装与解封KEMKEM常用于安全传输一个对称密钥。SM9的KEM机制相对直接但同样要小心。关键实现点共享秘密K的派生加密者生成随机数r计算C1 r * P2和g e(Ppub-e, Q_id)^r。这里g是GT群上的一个元素需要从中派生出密钥K。标准使用密钥派生函数KDF通常就是SM3的扩展用法。需要将g的坐标或某种规范化表示转换为字节串然后输入KDF。C1的编码与传输C1是一个椭圆曲线点需要序列化为字节串。通常使用压缩或未压缩格式。必须明确并记录你所使用的点编码格式解封方需要用同样的格式解析。解封时的配对计算解封方计算g e(C1, ds)。如果一切正确g应该等于加密方计算的g。这里再次用到双线性对的性质e(a*P, Q) e(P, Q)^a。# 示例KEM封装核心步骤概念性代码 def kem_encapsulate(master_pub_e, receiver_id: bytes, key_len: int32): KEM封装生成封装密文C和对称密钥K import secrets from sm9.math_utils import pairing, point_to_bytes from sm9.utils import kdf_sm3 # 假设实现了基于SM3的KDF # 1. 生成随机数 r in [1, N-1] r secrets.randbelow(N-1) 1 # 2. 计算 C r * P2 (在G2群) C1 r * P2 # 3. 推导接收者公钥 Q_id (在G1群用于加密) Q_id derive_encryption_pubkey(receiver_id) # 需要实现 # 4. 计算 w pairing(master_pub_e, Q_id) ^ r (在GT群) # master_pub_e 在 G2 Q_id 在 G1 w_base pairing(master_pub_e, Q_id) # e(Ppub-e, Q_id) # 这里需要计算GT群元素的指数运算依赖底层库的支持 # 假设我们的库支持 GT_element ** exponent w w_base ** r # 注意这是模幂运算在GT群的乘法循环子群上 # 5. 将w转换为字节串。如何序列化GT群元素是关键。 # 一种常见方法是将其投影到有限域F_{q^12}的某个基底下取系数编码。 w_bytes serialize_gt_element(w) # 需要实现 # 6. 使用KDF从w_bytes派生密钥K K kdf_sm3(w_bytes, key_len) # 7. 序列化C1 C1_bytes point_to_bytes(C1, compressedTrue) # 使用压缩格式节省空间 # 封装结果 C1_bytes encapsulation C1_bytes return encapsulation, K def kem_decapsulate(master_priv_e, receiver_id, encapsulation, key_len32): KEM解封从封装密文C中恢复对称密钥K # 1. 解析得到C1点 C1 bytes_to_point(encapsulation) # 需要实现 # 2. 获取接收者解密私钥 ds (在G1群) ds get_user_decrypt_private_key(master_priv_e, receiver_id) # 需要实现 # 3. 计算 w pairing(C1, ds) w_prime pairing(C1, ds) # C1在G2 ds在G1 # 4. 序列化 w 并派生密钥 K w_prime_bytes serialize_gt_element(w_prime) K_prime kdf_sm3(w_prime_bytes, key_len) return K_prime注意事项GT群元素的序列化和反序列化是一个难点。GT是F_{q^12}的一个乘法子群一个元素有12个F_q系数。你需要决定序列化哪些数据例如压缩一个坐标还是全部序列化并确保加解密双方使用完全相同的规则。最好在代码中写死一种规则并添加充分的注释。5. 测试与合规性验证代码写完了但离“可用”和“合规”还差最关键的一步测试。没有经过充分测试的密码实现无异于在悬崖边行走。5.1 标准测试向量的使用GM/T 0044-2016标准的附录里提供了详细的测试向量包括系统参数、密钥、中间值和最终结果。这是验证我们实现是否正确的“金标准”。测试策略单元测试为每一个核心函数如hash_to_point,sign,verify,encrypt,decrypt编写测试使用标准测试向量中的输入断言输出与标准值完全一致字节级一致。端到端测试模拟完整的业务流程例如用KGC生成主密钥和用户密钥对一条消息签名然后验证封装一个密钥然后解封确保恢复的密钥一致。随机性测试运行上千次随机生成的密钥和消息确保签名/验证、加密/解密始终成功没有因随机数不佳导致的失败。# 示例使用标准测试向量进行单元测试 (pytest格式) import pytest from sm9.core import SM9Signature from sm9.params import load_std_test_params def test_signature_with_standard_vector(): 使用标准附录C.1的测试向量 # 加载标准参数和测试数据 params, test_data load_std_test_params(signature) master_priv test_data[master_private_key] master_pub test_data[master_public_key] user_id test_data[user_id] message test_data[message] expected_signature test_data[expected_signature] # 字节串 # 初始化签名实例 sign_scheme SM9Signature(master_priv, master_pub) # 生成用户私钥 user_priv sign_scheme.generate_user_private_key(user_id) # 签名 signature sign_scheme.sign(user_priv, user_id, message) # 断言签名结果与标准值完全一致 assert signature expected_signature, f签名不匹配\n得到{signature.hex()}\n期望{expected_signature.hex()} # 验证签名应该成功 assert sign_scheme.verify(user_id, message, signature) # 篡改消息或签名验证应该失败 tampered_msg message bx assert not sign_scheme.verify(user_id, tampered_msg, signature) tampered_sig bytearray(signature) tampered_sig[-1] ^ 0x01 assert not sign_scheme.verify(user_id, message, bytes(tampered_sig))5.2 性能优化与安全加固考虑在通过正确性测试后我们需要考虑性能和更深层次的安全。预计算双线性对e(P1, P2)的计算非常耗时。对于固定的主公钥Ppub-e或Ppub-s可以预计算g e(P1, Ppub-s)等值在验证或密钥派生时直接使用能大幅提升性能。常数时间操作虽然Python解释器层面很难做到绝对的常数时间但我们可以避免一些明显的分支。例如在标量乘法中使用固定的循环次数不因标量的比特位是0而跳过操作。内存清理对于主私钥、用户私钥等敏感数据在使用后主动将其赋值为一个随机值或全零的等长字节串以对抗简单的内存扫描攻击。错误处理密码操作失败时应返回统一的错误类型如CryptoError而不是泄露具体的内部错误信息如“解密失败因为C1点不在曲线上”这有助于防止边信道攻击。6. 集成与商用密码应用安全性评估准备将我们实现的SM9库集成到实际应用中并为其通过密评做准备是最后的临门一脚。6.1 提供清晰的API与文档一个合格的密码库必须有清晰的API和详尽的文档。文档应包括安装说明如何安装依赖特别是那个底层的C扩展库。快速开始5分钟内完成密钥生成、签名验签的示例。API参考每个类、每个方法的详细说明包括参数类型、返回值、可能抛出的异常。安全警告明确指出哪些操作是危险的如使用不安全的随机数源、密钥该如何管理。符合性声明明确说明本实现遵循GM/T 0044-2016标准并通过了附录中的测试向量。6.2 应对密评的检查清单当测评机构来审查你的代码时他们可能会关注这些点。你可以提前准备好材料【随机数生成】展示所有调用随机数的地方都使用了secrets模块或os.urandom。【密钥管理】说明敏感密钥在内存中如何存储是否使用bytearray以便清零、在代码中是否硬编码、是否提供了安全的密钥导入/导出接口如使用口令加密的PKCS#8格式。【算法正确性】提供完整的标准测试向量运行结果报告以及你自己编写的随机测试结果。【代码审计】代码是否结构清晰、注释完整是否有明显的安全漏洞如缓冲区溢出——在Python中较少见但逻辑漏洞可能存在可以考虑引入静态代码分析工具如Bandit进行扫描。【依赖管理】你使用的底层数学库如bn256是否来自可信源是否有已知漏洞最好能提供其源码的哈希校验值。6.3 一个完整的应用示例基于SM9的API认证最后我们来看一个简单的应用场景使用SM9签名实现API请求的认证。# sm9_api_auth.py import json import time import hashlib from sm9.core import SM9Signature class SM9APIAuthenticator: def __init__(self, kgc_private_key, kgc_public_key, server_id): 初始化认证器。 server_id: 服务器的标识用于生成服务器端的签名私钥。 self.signer SM9Signature(kgc_private_key, kgc_public_key) # 假设KGC已经为这个server_id生成了私钥文件 self.server_priv_key self._load_server_private_key(server_id) self.server_id server_id def sign_request(self, method, path, bodyNone, timestampNone): 对API请求进行签名。 if timestamp is None: timestamp int(time.time()) # 1. 构造待签名的消息字符串 (规范非常重要双方必须一致) msg_dict { method: method.upper(), path: path, body_hash: hashlib.sm3(body if body else b).hexdigest() if body else , timestamp: timestamp } # 转换为规范化的JSON字符串无空格键排序固定 canonical_msg json.dumps(msg_dict, separators(,, :), sort_keysTrue) message canonical_msg.encode(utf-8) # 2. 使用SM9签名 signature self.signer.sign(self.server_priv_key, self.server_id, message) # 3. 将签名、时间戳、服务器ID等信息放入请求头 headers { X-SM9-Server-ID: self.server_id, X-SM9-Timestamp: str(timestamp), X-SM9-Signature: signature.hex() # 以16进制字符串传输 } return headers def verify_request(self, client_id, method, path, body, headers): 验证客户端发来的API请求签名。 try: sig_hex headers[X-SM9-Signature] timestamp int(headers[X-SM9-Timestamp]) # 1. 使用相同的规则构造消息 msg_dict { method: method.upper(), path: path, body_hash: hashlib.sm3(body if body else b).hexdigest() if body else , timestamp: timestamp } canonical_msg json.dumps(msg_dict, separators(,, :), sort_keysTrue) message canonical_msg.encode(utf-8) # 2. 验证签名 signature bytes.fromhex(sig_hex) # 注意这里验证时使用的是客户端的ID和消息 is_valid self.signer.verify(client_id, message, signature) # 3. (可选) 验证时间戳防止重放攻击 current_time int(time.time()) if abs(current_time - timestamp) 300: # 允许5分钟误差 return False, Timestamp expired return is_valid, Success if is_valid else Invalid signature except (KeyError, ValueError) as e: return False, fHeader parse error: {e} def _load_server_private_key(self, server_id): # 从安全存储如HSM、加密文件加载私钥 # 此处为示例直接生成。生产环境务必安全存储 return self.signer.generate_user_private_key(server_id) # 使用示例 if __name__ __main__: # 假设KGC已经设置好 kgc_priv ... # 从安全位置加载 kgc_pub ... # 从安全位置加载 authenticator SM9APIAuthenticator(kgc_priv, kgc_pub, api-server-01) # 服务器端签名一个请求 headers authenticator.sign_request(POST, /api/v1/data, bodyb{action: update}) print(Request Headers with SM9 Signature:, headers) # 客户端/验证端验证请求 (需要知道客户端的ID例如client-alice) # 假设我们收到了请求和上面的headers is_valid, msg authenticator.verify_request( client_idclient-alice, methodPOST, path/api/v1/data, bodyb{action: update}, headersheaders ) print(fVerification Result: {is_valid}, Message: {msg})在这个示例中最关键的是消息的规范化。服务器和客户端必须用完全相同的规则相同的字段、相同的顺序、相同的编码来构造待签名的消息字节串否则验证必然失败。这通常需要定义一个明确的“签名规范”文档。实现一个合规、安全、可用的SM9算法库是一个将密码学标准翻译成可靠代码的系统工程。它要求开发者不仅理解算法原理更要具备密码工程的安全意识。从吃透标准文档到选择合适的基础库再到实现每一个细节并规避各种陷阱最后通过严格的测试和审计每一步都需要耐心和严谨。这个过程虽然充满挑战但当你看到自己的实现能够完美通过所有标准测试并成功集成到实际系统中时那种成就感是无可替代的。希望这篇详细的解析能为你自己的SM9实现之路扫清一些障碍。