<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; SSH</title>
	<atom:link href="http://tonybai.com/tag/ssh/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sat, 04 Apr 2026 00:51:31 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>权威认证：Go核心密码学库通过独立安全审计</title>
		<link>https://tonybai.com/2025/05/21/go-crypto-audit/</link>
		<comments>https://tonybai.com/2025/05/21/go-crypto-audit/#comments</comments>
		<pubDate>Wed, 21 May 2025 13:20:13 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Argon2]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[ECC]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[ECDSA]]></category>
		<category><![CDATA[FIPS-140]]></category>
		<category><![CDATA[fips-203]]></category>
		<category><![CDATA[FIPS-204]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HMAC]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[KEM]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[PQC]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Shor]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[X25519MLKEM768]]></category>
		<category><![CDATA[后量子密码]]></category>
		<category><![CDATA[后量子密码学]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[量子计算]]></category>
		<category><![CDATA[随机数]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4737</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/21/go-crypto-audit 大家好，我是 Tony Bai。 信息安全是我们数字时代的基石。对于 Go 语言而言，其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日，Go 官方博客发布了一篇重要文章，详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的安全审计结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入，也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。 好消息是：审计结果非常积极！ 仅发现一个低风险问题（已在 Go 1.25 开发分支修复，且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成）和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。 在这篇文章中，我们就来介绍这一对Go密码学领域具有里程碑意义的事件。 安全审计的范围与 Go 的密码学设计原则 Trail of Bits 的审计范围广泛，涵盖了 Go 标准库中核心的加密组件，这些组件同时也是新的原生FIPS 140-3模块的验证部分。具体包括： 密钥交换： ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。 数字签名： ECDSA, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-crypto-audit-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/21/go-crypto-audit">本文永久链接</a> &#8211; https://tonybai.com/2025/05/21/go-crypto-audit</p>
<p>大家好，我是 Tony Bai。</p>
<p>信息安全是我们数字时代的基石。对于 Go 语言而言，其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日，<a href="https://go.dev/blog/tob-crypto-audit">Go 官方博客发布了一篇重要文章</a>，详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的<strong>安全审计</strong>结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入，也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。</p>
<p><strong>好消息是：审计结果非常积极！</strong> 仅发现一个低风险问题（已在 Go 1.25 开发分支修复，且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成）和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。</p>
<p>在这篇文章中，我们就来介绍这一对Go密码学领域具有里程碑意义的事件。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<h2>安全审计的范围与 Go 的密码学设计原则</h2>
<p>Trail of Bits 的审计范围广泛，涵盖了 Go 标准库中核心的加密组件，这些组件同时也是新的原生<a href="https://tonybai.com/2024/11/16/go-crypto-and-fips-140">FIPS 140-3</a>模块的验证部分。具体包括：</p>
<ul>
<li><strong>密钥交换：</strong> ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。</li>
<li><strong>数字签名：</strong> ECDSA, RSA, 和 Ed25519。</li>
<li><strong>加密算法：</strong> AES-GCM, AES-CBC, 和 AES-CTR。</li>
<li><strong>哈希函数：</strong> SHA-1, SHA-2, 和 SHA-3。</li>
<li><strong>密钥派生：</strong> HKDF 和 PBKDF2。</li>
<li><strong>认证机制：</strong> HMAC。</li>
<li><strong>密码学随机数生成器。</strong></li>
<li><strong>底层大整数和椭圆曲线实现</strong> (包括其精巧的汇编核心)。</li>
</ul>
<p>值得注意的是，像 TLS 和 X.509 这样的高层协议未在此次审计范围内。</p>
<p>Go 团队在博客中强调，他们对密码学包的安全性保障源于多方面的努力：</p>
<ol>
<li><strong>积极限制复杂性：</strong> 遵循“密码学原则”，例如优先考虑安全性而非极致性能。</li>
<li><strong>彻底的测试：</strong> 采用多种技术进行广泛测试。</li>
<li><strong>安全 API 的内部利用：</strong> 即使是内部包也倾向于使用安全的 API。</li>
<li><strong>利用 Go 语言特性：</strong> 避免常见的内存管理问题。</li>
<li><strong>注重可读性：</strong> 便于维护、代码审查和审计。</li>
</ol>
<h2>审计发现：低风险问题与建议性信息</h2>
<h3>一个低风险发现：Go+BoringCrypto 内存管理</h3>
<p>审计中唯一被标记为具有潜在可利用性的问题 (TOB-GOCL-3) 是一个<strong>低风险</strong>问题，影响小且难以触发。该问题涉及已废弃且不受支持的、基于 CGO 的 Go+BoringCrypto 集成中的内存管理。</p>
<p><strong>关键点：</strong></p>
<ul>
<li>此问题已在 <strong>Go 1.25 的开发分支中修复</strong>。</li>
<li>Go+BoringCrypto GOEXPERIMENT <strong>默认不启用</strong>，且<strong>不被 Go 团队支持在 Google 外部使用</strong>。</li>
<li>这个问题进一步坚定了 Go 团队<strong>转向原生 FIPS 140-3 模式</strong>的决心，该模式使用纯 Go 实现的密码学包，避免了 CGO 交互的复杂性和手动内存管理的风险。</li>
</ul>
<h3>五个建议性信息：与安全最佳实践息息相关</h3>
<p>其余五个发现本质上是<strong>建议性的 (informational)</strong>，不构成直接的安全风险，但与安全最佳实践相关。这些建议也已在 Go 1.25 开发分支中得到处理。</p>
<p>这些建议主要涉及：</p>
<ul>
<li><strong>潜在的计时侧信道 (Timing Side-Channels)：</strong> (TOB-GOCL-1, TOB-GOCL-2, TOB-GOCL-6)
<ul>
<li>crypto/ecdh, crypto/ecdsa: 字节到字段元素的转换非恒定时间。Go 团队决定将其改为恒定时间，以防未来被意外用于处理秘密值。</li>
<li>crypto/ecdsa: P-256 条件否定的 Power ISA 汇编实现非恒定时间 (CVE-2025-22866)。已与 IBM 合作修复。</li>
<li>crypto/ed25519: 标量内部外部表示转换非恒定时间。同样改为恒定时间。<br />
这些操作在现有用法中主要处理公开输入（如公钥），因此不被视为直接安全问题，但为了更强的鲁棒性和避免未来误用，Go 团队选择进行恒定时间修复。</li>
</ul>
</li>
<li><strong>内部 API 的误用风险：</strong> (TOB-GOCL-4)
<ul>
<li>crypto/internal/fips140/drbg: CTR_DRBG API 存在误用风险。由于此实现范围严格限定且未公开导出，Go 团队认为可接受，并通过文档警告明确了其限制。</li>
</ul>
</li>
<li><strong>实现完整性：</strong> (TOB-GOCL-5)
<ul>
<li>crypto/pbkdf2: 未强制执行 RFC 8018 中定义的输出长度限制。虽然实际中不太可能生成超长密钥（例如，使用 SHA-256 时超过 137GB），但为符合标准，此限制已被添加。</li>
</ul>
</li>
</ul>
<p>这些发现和修复再次体现了 Go 团队对密码学安全性的极致追求和透明度。</p>
<h2>Go 密码学的未来：PQC、FIPS 与更易用的 API</h2>
<p>审计结果令人鼓舞，但 Go 团队并未止步。他们正积极推进 Go 密码学库的现代化和易用性。</p>
<p><strong>1. 原生 FIPS 140-3 模式：</strong></p>
<p><strong>Go 1.24 已经包含了一个纯 Go 实现的 FIPS 140-3 模式</strong>，目前正在接受 CMVP (Cryptographic Module Validation Program) 测试。这将为所有 Go 用户提供一个受支持的、符合 FIPS 140-3 标准的密码学模式，取代之前不受支持的 Go+BoringCrypto 集成。</p>
<p><strong>2. 后量子密码学 (PQC) 的全面推进：</strong></p>
<p>正如我们之前讨论的，PQC 是应对未来量子计算机威胁的关键。Go 团队在这方面的工作取得了显著进展：</p>
<ul>
<li><strong>crypto/mlkem 包已在 Go 1.24 中正式引入：</strong> 实现了 ML-KEM-768 和 ML-KEM-1024，为开发者提供了直接使用后量子密钥封装机制的能力。</li>
<li><strong>crypto/tls 默认启用 X25519MLKEM768：</strong> <strong>Go 1.24 的一个重大更新！</strong> crypto/tls 包现在<strong>默认启用</strong>了 X25519MLKEM768 混合密钥交换机制（当 tls.Config.CurvePreferences 为 nil 时）。这意味着 Go 应用可以更轻松地获得针对经典和量子攻击的双重保护。开发者可以通过设置 CurvePreferences 或 GODEBUG=tlsmlkem=0 来控制此行为。</li>
</ul>
<p>以上两点在我的《<a href="https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go">未雨绸缪：Go开发者需要了解的后量子密码学与实现现状</a>》一文中有详细介绍。</p>
<p><strong>3. 更易用的高层密码学 API：</strong></p>
<p>Go 团队计划引入新的、更易用的高层密码学 API，旨在降低开发者选择和使用高质量加密算法的门槛。首个目标是要简化的密码哈希 API：** 计划提供一个简单的密码哈希 API，让用户无需纠结于选择众多可能的算法（如 bcrypt, scrypt, Argon2 等），并包含机制以在技术发展时自动迁移到更新的算法。这对于提升应用安全性至关重要。</p>
<h2>Go 与 PQC：机遇、挑战及开发者行动</h2>
<p>Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24已将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇。独立安全审计的积极结果进一步增强了社区对 Go 密码学库的信心。然而，PQC 技术的相对新颖性也带来了 API 演进、行业经验积累及潜在资源消耗（如密钥和签名尺寸增大）等方面的挑战。Go 社区正通过提供清晰文档、默认安全配置和推动行业实践来赋能开发者，共同塑造一个更安全的数字未来。</p>
<p>面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为（特别是 X25519MLKEM768 的默认启用）及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展（如对 ML-DSA 签名算法的支持）以及规划中的易用性 API（如密码哈希 API）。利用 Go 1.24 提供的坚实工具，并结合安全审计带来的信心，为应用的未来安全做好充分准备。</p>
<h2>小结：安全为本，Go 在密码学领域持续精进</h2>
<p>Go 官方对核心密码学库进行独立安全审计，并公开透明地分享结果和改进措施，再次彰显了其对安全性的坚定承诺。审计结果的积极性，结合 Go 1.24 在 PQC 和 FIPS 合规性方面的重大进展，无疑为 Go 开发者提供了更强大的信心和更先进的工具来构建安全的应用程序。</p>
<p>crypto/mlkem 的加入和 crypto/tls 中 X25519MLKEM768 的默认启用，标志着 Go 在后量子密码实用化方面迈出了重要一步。未来规划中更易用的高层密码学 API，如简化的密码哈希接口，将进一步降低安全开发的门槛，帮助开发者构建更加健壮和面向未来的系统。</p>
<p>这不仅是一系列技术更新，更是 Go 社区共同迈向更安全数字时代的体现。让我们持续关注 Go 在密码学领域的努力，积极采纳最佳实践，共同为构建一个能抵御未来威胁的数字世界贡献力量。</p>
<hr />
<p><strong>欢迎加入我的知识星球 “Go &amp; AI 精进营”！</strong></p>
<p>在这里，我们将一起：</p>
<ul>
<li>追踪 Go 官方在密码学和 PQC 领域的最新动态和深度解读。</li>
<li>分享和讨论 Go 安全编程的实战经验和代码示例。</li>
<li>探讨 PQC 算法选型、API 设计和性能优化。</li>
<li>构建一个积极、专业的 Go 安全技术交流社群。</li>
</ul>
<p>现在就扫描下方二维码加入星球，与更多 Gopher 一起，共同探索和构建更安全的 Go 应用，为量子未来做好准备！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>感谢您的阅读！</p>
<p>如果这篇文章让您对 Go 语言的密码学安全和 PQC 进展有了更清晰的认识，请不吝点赞、转发，让更多关注 Go 和信息安全的朋友们了解到这些重要动态！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/21/go-crypto-audit/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>未雨绸缪：Go开发者需要了解的后量子密码学与实现现状</title>
		<link>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/</link>
		<comments>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/#comments</comments>
		<pubDate>Tue, 20 May 2025 00:15:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Argon2]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[ECC]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[FIPS-140]]></category>
		<category><![CDATA[fips-203]]></category>
		<category><![CDATA[FIPS-204]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[KEM]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[PQC]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Shor]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[X25519MLKEM768]]></category>
		<category><![CDATA[后量子密码学]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[量子计算]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4730</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go 大家好，我是 Tony Bai。 在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——量子计算机。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。 这不是科幻电影，而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此，“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生，旨在研发能够抵御量子计算机攻击的新一代密码算法。 作为 Go 开发者，我们或许觉得量子计算机还很遥远，但“现在记录数据，未来量子破解”的风险已然存在。更重要的是，Go 语言作为一门以简洁、高效和安全著称的现代编程语言，其核心团队早已在为这个“后量子时代”积极布局。随着 Go 1.24 的发布，这一布局取得了实质性的进展：备受期待的 crypto/mlkem 包正式加入标准库！ 那么，PQC 究竟是什么？crypto/mlkem 包为我们带来了什么？Go 语言在 PQC 的浪潮中又将扮演怎样的角色？今天，就让我们一起“未雨绸缪”，深入了解 PQC 及其在 Go 中的最新进展。 量子风暴将至：为何我们需要 PQC？ 想象一下，你用 RSA 加密了公司的核心商业机密，或者用 ECDSA 签名了重要的合同。这些操作的安全性，都依赖于经典计算机难以在有效时间内解决某些数学难题（如大数分解、离散对数）。 然而，量子计算机一旦足够强大，Shor 算法就能在多项式时间内攻破这些难题。这意味着： 加密通讯不再私密： HTTPS、VPN 等都可能被破解。 数字签名不再可信： 软件更新、代码签名、身份认证都可能被伪造。 历史数据面临风险： 黑客现在就可以截获并存储加密数据，等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等，这无疑是巨大威胁。 这就是我们迫切需要 PQC 的原因：寻找并标准化那些即使是量子计算机也难以破解的新密码算法。 PQC 的曙光：NIST 标准化与主流算法 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/post-quantum-cryptography-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go</p>
<p>大家好，我是 Tony Bai。</p>
<p>在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——<strong>量子计算机</strong>。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。</p>
<p>这不是科幻电影，而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此，“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生，旨在研发能够抵御量子计算机攻击的新一代密码算法。</p>
<p>作为 Go 开发者，我们或许觉得量子计算机还很遥远，但“现在记录数据，未来量子破解”的风险已然存在。更重要的是，Go 语言作为一门以简洁、高效和安全著称的现代编程语言，其核心团队早已在为这个“后量子时代”积极布局。<strong>随着 Go 1.24 的发布，这一布局取得了实质性的进展：备受期待的 crypto/mlkem 包正式加入标准库！</strong></p>
<p>那么，PQC 究竟是什么？crypto/mlkem 包为我们带来了什么？Go 语言在 PQC 的浪潮中又将扮演怎样的角色？今天，就让我们一起“未雨绸缪”，深入了解 PQC 及其在 Go 中的最新进展。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<h2>量子风暴将至：为何我们需要 PQC？</h2>
<p>想象一下，你用 RSA 加密了公司的核心商业机密，或者用 ECDSA 签名了重要的合同。这些操作的安全性，都依赖于经典计算机难以在有效时间内解决某些数学难题（如大数分解、离散对数）。</p>
<p>然而，量子计算机一旦足够强大，<a href="https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go">Shor 算法</a>就能在多项式时间内攻破这些难题。这意味着：</p>
<ul>
<li><strong>加密通讯不再私密：</strong> HTTPS、VPN 等都可能被破解。</li>
<li><strong>数字签名不再可信：</strong> 软件更新、代码签名、身份认证都可能被伪造。</li>
<li><strong>历史数据面临风险：</strong> 黑客现在就可以截获并存储加密数据，等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等，这无疑是巨大威胁。</li>
</ul>
<p>这就是我们迫切需要 PQC 的原因：<strong>寻找并标准化那些即使是量子计算机也难以破解的新密码算法。</strong></p>
<h2>PQC 的曙光：NIST 标准化与主流算法</h2>
<p>幸运的是，我们并非束手无策。在应对后量子密码学（PQC）的挑战时，美国国家标准与技术研究院（NIST）自2016年启动了PQ算法的标准化进程，旨在筛选和确立新一代的密码算法。经过多轮评审，几种优胜算法逐渐浮出水面，为未来的安全通信提供了希望。</p>
<p>首先，在密钥封装/交换机制（KEM &#8211; Key Encapsulation Mechanism)方面，基于格密码学（Lattice-based cryptography）的ML-KEM被选为主要的KEM标准（FIPS 203）。这一算法的优势在于，某些实现的性能甚至超过了我们熟知的X25519密钥交换算法，为其广泛应用奠定了基础。</p>
<p>简单来说，KEM的工作原理可以类比于使用一个特殊的“量子安全信封”——公钥，将对称密钥（例如AES密钥）封装后发送给对方。接收方使用对应的“量子安全钥匙”——私钥，打开信封取出密钥，随后双方便可借助这个对称密钥进行安全的通信。</p>
<p>在数字签名方面，ML-DSA基于Dilithium算法，同样属于格密码学的范畴。该算法被选为主要的数字签名标准（FIPS 204），用于验证信息的来源及完整性。这为数字通信的安全性提供了重要保障。</p>
<p>通过这些新兴的密码算法，NIST为抵御未来的量子攻击奠定了坚实的基础，展现了在后量子时代中应对安全挑战的希望。</p>
<p><strong>PQC 算法的挑战：更大的“块头”</strong></p>
<p>虽然 PQC 算法带来了量子抵抗性，但也普遍面临一个挑战：<strong>密钥和签名的尺寸通常比经典算法大得多。</strong> 这可能会对网络带宽、存储空间（尤其是 X.509 证书）以及资源受限设备带来一定压力。</p>
<p><strong>现有算法与 PQC 替换畅想 (简表):</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/post-quantum-cryptography-in-go-2.png" alt="" /></p>
<blockquote>
<p>注意：上表为简化对应，实际替换过程会更复杂。</p>
</blockquote>
<h2>Go语言与PQC：Go 1.24 迎来 crypto/mlkem</h2>
<p>Go 语言以其强大的 crypto 标准库和对安全性的重视而闻名。面对 PQC 的浪潮，Go 核心团队自然不会缺席。他们的策略是<strong>谨慎、务实且前瞻</strong>。</p>
<h3>Go 密码学库的坚实基础</h3>
<p>在讨论 PQC 之前，值得一提的是 Go 现有密码学库的优秀设计：</p>
<ul>
<li><strong>简洁易用：</strong> 尽量减少复杂性，提供安全的默认值，降低开发者误用风险。</li>
<li><strong>持续现代化：</strong> 如对 RSA 后端的优化、新增 crypto/ecdh 包简化密钥交换、通过 godebug 机制平滑引入安全改进等。</li>
<li><strong>golang.org/x/crypto：</strong> 作为标准库的扩展和试验田，引入 ChaCha20、Argon2 等高级算法。</li>
</ul>
<p>在 crypto/mlkem 正式发布之前，Go 团队已在早期版本（如 Go 1.23）中进行了内部实现和集成工作。这些工作为标准库的最终引入奠定了基础，尤其是在 crypto/tls 包中探索对后量子密钥交换的支持。</p>
<h3>Go 1.24：crypto/mlkem 包正式发布！</h3>
<p><strong>激动人心的时刻终于到来！Go 1.24 版本正式将 crypto/mlkem 包引入标准库。</strong> 这一里程碑事件由 Go 核心开发者 FiloSottile（Filippo Valsorda）在 <a href="https://github.com/golang/go/issues/70122">Go Issue #70122</a> 中提议并推动实现。</p>
<p>crypto/mlkem 包实现了 FIPS 203 标准中定义的 ML-KEM 算法，目前支持以下两个参数集：</p>
<ul>
<li><strong>ML-KEM-768:</strong> 这是在大多数场景中推荐使用的参数集，提供了足够的后量子安全性。</li>
<li><strong>ML-KEM-1024:</strong> 主要用于满足 CNSA 2.0 等特定规范的要求。</li>
</ul>
<p>Go 团队暂时未包含 ML-KEM-512，因为它在实际部署中较为罕见。这一选择与 BoringSSL 等其他主流密码库的实现保持一致。</p>
<p>下面是crypto/mlkem 包 API的一些设计考量：</p>
<ul>
<li><strong>类型安全：</strong> 为 ML-KEM-768 和 ML-KEM-1024 提供了独立的类型（如 mlkem.DecapsulationKey768 和 mlkem.EncapsulationKey768），避免了因参数集不同导致数据结构大小不匹配的问题。虽然数字后缀在 godoc 中的排序可能不理想，但这是为了类型清晰和性能考虑的权衡。</li>
<li><strong>种子作为解封装密钥：</strong> 解封装密钥（私钥）支持以 64 字节的种子（”d || z” 形式）进行创建和表示。这种格式与 IETF 的方向一致，有助于标准化和互操作性。</li>
<li><strong>简洁的核心操作：</strong> API 围绕密钥生成 (GenerateKey)、密钥解析 (NewDecapsulationKey/NewEncapsulationKey)、密钥字节化 (Bytes)、封装 (Encapsulate) 和解封装 (Decapsulate) 这几个核心 KEM 操作展开。</li>
</ul>
<h3>crypto/mlkem 使用示例</h3>
<p>下面是一个演示如何使用 crypto/mlkem 包进行密钥封装和解封装的基本流程：</p>
<pre><code class="go">package main

import (
    "bytes"
    "crypto/mlkem"
    "fmt"
    "log"
)

func main() {
    // === 场景：Alice 希望与 Bob 安全地共享一个密钥 ===

    // --- Alice 的操作 ---
    // 1. Alice 生成一个 ML-KEM-768 私钥 (DecapsulationKey)。
    //    GenerateKey768 返回 (*DecapsulationKey768, error)。
    privateKeyAlice, err := mlkem.GenerateKey768()
    if err != nil {
        log.Fatalf("Alice: Failed to generate ML-KEM-768 decapsulation key: %v", err)
    }

    // 2. 从私钥获取对应的公钥 (EncapsulationKey)。
    publicKeyAlice := privateKeyAlice.EncapsulationKey()

    // 3. Alice 将她的公钥序列化为字节串，以便发送给 Bob。
    publicKeyAliceBytes := publicKeyAlice.Bytes()
    fmt.Printf("Alice's Public Key (ML-KEM-768, %d bytes): %x...\n", len(publicKeyAliceBytes), publicKeyAliceBytes[:16])

    // --- Bob 的操作 ---
    // Bob 接收到 Alice 的公钥字节串 publicKeyAliceBytes

    // 4. Bob 根据接收到的字节串创建一个公钥实例。
    //    NewEncapsulationKey768 返回 (*EncapsulationKey768, error)。
    publicKeyReceivedByBob, err := mlkem.NewEncapsulationKey768(publicKeyAliceBytes)
    if err != nil {
        log.Fatalf("Bob: Failed to parse Alice's public key: %v", err)
    }

    // 5. Bob 使用 Alice 的公钥来封装一个新的共享密钥。
    //    Encapsulate 返回 (sharedKey []byte, ciphertext []byte)，不返回 error。
    sharedKeyForBob, ciphertextForAlice := publicKeyReceivedByBob.Encapsulate()
    fmt.Printf("Bob: Generated Shared Key (ML-KEM-768, %d bytes): %x\n", len(sharedKeyForBob), sharedKeyForBob)
    fmt.Printf("Bob: Generated Ciphertext for Alice (%d bytes): %x...\n", len(ciphertextForAlice), ciphertextForAlice[:16])

    // --- Alice 的操作 ---
    // Alice 接收到 Bob 发送过来的密文 ciphertextForAlice

    // 6. Alice 使用她的私钥和收到的密文来解封装，得到共享密钥。
    //    Decapsulate 返回 (sharedKey []byte, error)。
    sharedKeyForAlice, err := privateKeyAlice.Decapsulate(ciphertextForAlice)
    if err != nil {
        // 如果密文无效或已被篡改，Decapsulate 会返回错误。
        log.Fatalf("Alice: Failed to decapsulate shared key: %v", err)
    }
    fmt.Printf("Alice: Decapsulated Shared Key (ML-KEM-768, %d bytes): %x\n", len(sharedKeyForAlice), sharedKeyForAlice)

    // --- 验证 ---
    // 7. 验证 Alice 和 Bob 得到的共享密钥是否一致。
    if bytes.Equal(sharedKeyForAlice, sharedKeyForBob) {
        fmt.Println("\nSuccess! Alice and Bob now share the same secret key using ML-KEM-768.")
    } else {
        // 这通常不应该发生，如果 Decapsulate 成功且数据未被篡改。
        fmt.Println("\nError! Shared keys do NOT match. This is unexpected.")
    }

    // 简单演示 ML-KEM-1024 的密钥生成 (API 结构类似)
    dk1024, err := mlkem.GenerateKey1024()
    if err != nil {
        log.Fatalf("Failed to generate ML-KEM-1024 decapsulation key: %v", err)
    }
    _ = dk1024.EncapsulationKey() // 获取公钥
    fmt.Println("\nSuccessfully demonstrated ML-KEM-1024 key generation as well.")

    // 打印一些常量信息
    fmt.Printf("\nML-KEM Constants:\n")
    fmt.Printf("  SharedKeySize: %d bytes\n", mlkem.SharedKeySize)
    fmt.Printf("  SeedSize: %d bytes\n", mlkem.SeedSize)
    fmt.Printf("  CiphertextSize768: %d bytes\n", mlkem.CiphertextSize768)
    fmt.Printf("  EncapsulationKeySize768: %d bytes\n", mlkem.EncapsulationKeySize768)
    fmt.Printf("  CiphertextSize1024: %d bytes\n", mlkem.CiphertextSize1024)
    fmt.Printf("  EncapsulationKeySize1024: %d bytes\n", mlkem.EncapsulationKeySize1024)
}
</code></pre>
<p>使用Go 1.24+版本运行上述代码，可以得到类似如下输出结果：</p>
<pre><code>Alice's Public Key (ML-KEM-768, 1184 bytes): f880089a159c9ba338a684c70e10bdee...
Bob: Generated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f
Bob: Generated Ciphertext for Alice (1088 bytes): 9afe9f9d36a581a5d7e47b7913c65886...
Alice: Decapsulated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f

Success! Alice and Bob now share the same secret key using ML-KEM-768.

Successfully demonstrated ML-KEM-1024 key generation as well.

ML-KEM Constants:
  SharedKeySize: 32 bytes
  SeedSize: 64 bytes
  CiphertextSize768: 1088 bytes
  EncapsulationKeySize768: 1184 bytes
  CiphertextSize1024: 1568 bytes
  EncapsulationKeySize1024: 1568 bytes
</code></pre>
<p>上述代码仅为了展示 crypto/mlkem 包的核心用法。实际应用中，公钥和密文的传输需要通过网络等信道。</p>
<p>crypto/mlkem 包的加入，使得 Go 开发者可以直接在应用层使用标准化的后量子密钥封装机制，为构建面向未来的安全应用提供了坚实的基础。</p>
<h3>crypto/tls 的 PQC 集成：Go 1.24 默认启用，让 HTTPS 更“抗量子”</h3>
<p>对于大多数 Go 开发者而言，直接使用底层的 crypto/mlkem 包可能不是最常见的场景。更令人振奋的是，Go 团队已将后量子密码能力无缝集成到了我们日常使用的 crypto/tls 包中！</p>
<p><strong>根据 Go 1.24 的发布说明，crypto/tls 包现在默认支持并启用了新的后量子混合密钥交换机制 X25519MLKEM768。</strong></p>
<p>这意味着什么呢？</p>
<ul>
<li><strong>默认的后量子保护：</strong> 当你的 Go 1.24+ 应用程序使用 crypto/tls（例如，作为 HTTPS 服务器或客户端），并且 tls.Config 中的 CurvePreferences 字段未被显式设置（保持为 nil）时，TLS 握手将<strong>自动尝试使用 X25519MLKEM768 进行密钥交换</strong>。</li>
<li><strong>混合机制的优势：</strong> X25519MLKEM768 是一种<strong>混合 (hybrid)</strong> 密钥交换方案。它巧妙地将经过广泛验证的经典椭圆曲线算法 X25519 与后量子安全的 ML-KEM-768 结合起来。这样做的好处是：
<ul>
<li><strong>经典安全：</strong> 即使 ML-KEM-768 未来被发现存在未知的弱点（尽管可能性很小），X25519 依然能提供经典的安全性。</li>
<li><strong>量子抵抗：</strong> 面对量子计算机的威胁，ML-KEM-768 部分提供了后量子保护。</li>
</ul>
</li>
</ul>
<p>这种“两全其美”的设计是当前 PQC 过渡阶段推荐的主流方案。</p>
<ul>
<li><strong>开发者体验的极致简化：</strong> 大部分情况下，开发者<strong>无需修改现有代码</strong>即可获得这种增强的安全性。Go 语言的哲学再次体现——将复杂性封装起来，提供安全且易用的默认行为。</li>
</ul>
<p><strong>如何控制这一行为？</strong></p>
<p>虽然默认启用是推荐的，但 Go 团队也考虑到了现实世界中的兼容性问题。在某些情况下，一些老旧或有缺陷的 TLS 服务器可能无法正确处理 X25519MLKEM768 握手过程中可能产生的较大记录 (TLS record)，导致握手超时失败。</p>
<p>因此，Go 1.24 提供了禁用此默认行为的选项：</p>
<ul>
<li><strong>通过 tls.Config.CurvePreferences：</strong></li>
</ul>
<p>你可以显式设置 CurvePreferences 字段，只包含你希望支持的经典密钥交换机制，从而排除 X25519MLKEM768。</p>
<pre><code class="go">// 示例：显式设置，不包含 X25519MLKEM768
config := &amp;tls.Config{
    Certificates:     []tls.Certificate{cert},
    CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, // 明确指定经典曲线
}
</code></pre>
<ul>
<li><strong>通过 GODEBUG 环境变量：</strong></li>
</ul>
<p>可以在运行时通过设置环境变量 GODEBUG=tlsmlkem=0 来全局禁用 X25519MLKEM768 的默认启用。</p>
<p><strong>概念性代码示例（体现 Go 1.24 默认行为）：</strong></p>
<pre><code class="go">package main

import (
    "crypto/tls"
    "log"
    "net/http"
)

func helloServer(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server with Go 1.24 TLS.\n"))
    // Go 1.24 crypto/tls 默认会尝试 X25519MLKEM768 密钥交换
    // 如果客户端也支持，连接将具备后量子安全性！
}

func main() {
    // 加载证书和私钥 (此处省略具体加载过程)
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        log.Fatalf("server: loadkeys: %s", err)
    }

    // 服务器配置
    // 在 Go 1.24+ 中，如果 CurvePreferences 为 nil (默认)，
    // X25519MLKEM768 将被默认启用。
    config := &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
        // CurvePreferences: nil, // 默认即启用 X25519MLKEM768
    }

    http.HandleFunc("/hello", helloServer)
    server := &amp;http.Server{
        Addr:      ":8443",
        TLSConfig: config,
    }
    log.Println("Starting server on https://localhost:8443/hello")
    log.Fatal(server.ListenAndServeTLS("", "")) // 使用空字符串让其加载 config 中的证书
}

// 客户端示例 (概念)
// clientConfig := &amp;tls.Config{
// InsecureSkipVerify: true, // 仅用于测试，生产环境不要用
//     CurvePreferences: nil, // 客户端也会默认尝试 X25519MLKEM768
// }
// conn, err := tls.Dial("tcp", "localhost:8443", clientConfig)
// if err != nil {
//     log.Fatalf("client: dial: %s", err)
// }
// defer conn.Close()
// log.Println("client: connected to: ", conn.RemoteAddr())
</code></pre>
<p><strong>重要提示：</strong> Go 1.24 还<strong>移除了对实验性 X25519Kyber768Draft00 密钥交换的支持</strong>，完全转向了标准化的 X25519MLKEM768。</p>
<p>crypto/tls 中这一默认的后量子安全增强，是 Go 语言在 PQC 时代向前迈出的坚实一步，极大地降低了开发者应用 PQC 的门槛。</p>
<p>开发者可能只需要更新 Go 版本，或者做少量配置，就能让应用具备初步的后量子防护能力，而无需深入了解 ML-KEM 的复杂细节。这正是 Go 追求简洁易用哲学的体现。</p>
<p>对于 SSH 协议，Go 团队计划密切关注 OpenSSH 的发展。一旦 OpenSSH 支持 NIST 选定的 ML-KEM 标准（目前 OpenSSH 使用的是 NTRU，非 NIST 主选），Go 团队也将在 crypto/ssh 包中添加相应支持，以确保互操作性。</p>
<h2>Go 与 PQC：机遇、挑战及开发者行动</h2>
<p>Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24 将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇，但也伴随着 API 演进、行业经验不足及资源消耗等挑战。Go 社区正通过提供清晰文档和推动实践来赋能开发者，共同塑造一个更安全的数字未来。</p>
<p>面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展，并参与社区交流，为应用的未来安全做好规划。Go 1.24 已为我们迈向后量子安全提供了坚实的工具。</p>
<h2>小结：为量子未来，Go 已在路上</h2>
<p>后量子密码学不再是遥不可及的未来概念，而是关乎我们数字世界长期安全的关键一步。Go 语言凭借其在密码学领域的深厚积累和前瞻性布局，正稳步迈向这个新时代。</p>
<p>Go 1.24 中 crypto/mlkem 包的正式发布，是 Go 在 PQC 领域的一个重要里程碑。它为开发者提供了直接、标准化的工具来应对量子计算的潜在威胁。结合未来在 crypto/tls 和 crypto/ssh 等包中的 PQC 集成，Go 团队正努力为开发者提供安全、易用且高效的后量子密码解决方案。</p>
<p>这不仅是一场技术升级，更是 Go 社区共同承担的责任。通过学习、关注、参与和实践，我们可以与 Go 一道，为构建一个能抵御未来量子威胁的、更安全的数字世界贡献力量。让我们一起期待 Go 在后量子时代的精彩表现！</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/issues/70122">crypto/mlkem: new package #70122</a> &#8211; https://github.com/golang/go/issues/70122</li>
<li><a href="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards">NIST Releases First 3 Finalized Post-Quantum Encryption Standards</a> &#8211; https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards </li>
<li><a href="https://csrc.nist.gov/projects/post-quantum-cryptography">Post-Quantum Cryptography</a> &#8211; https://csrc.nist.gov/projects/post-quantum-cryptography</li>
<li><a href="https://en.wikipedia.org/wiki/Post-quantum_cryptography">Post-quantum cryptography</a> &#8211; https://en.wikipedia.org/wiki/Post-quantum_cryptography</li>
<li><a href="https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/">Towards Post-Quantum Cryptography in TLS</a> &#8211; https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/</li>
<li><a href="https://words.filippo.io/dispatches/post-quantum-age/">KEMS AND POST-QUANTUM AGE</a> &#8211; https://words.filippo.io/dispatches/post-quantum-age/ </li>
<li><a href="https://words.filippo.io/dispatches/mlkem768/">POST-QUANTUM CRYPTOGRAPHY FOR THE GO ECOSYSTEM</a> &#8211; https://words.filippo.io/dispatches/mlkem768/</li>
<li><a href="https://changelog.com/gotime/295?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 1</a> &#8211; https://changelog.com/gotime/295</li>
<li><a href="https://changelog.com/gotime/298?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 2</a> &#8211; https://changelog.com/gotime/298</li>
<li><a href="https://changelog.com/gotime/313?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 3</a> &#8211; https://changelog.com/gotime/313</li>
<li><a href="https://medium.com/cyberark-engineering/a-post-quantum-cryptography-web-server-in-go-1-23-9f7e98db7b39">Post Quantum Cryptography Web Server in Go 1.23</a> &#8211; https://medium.com/cyberark-engineering/a-post-quantum-cryptography-web-server-in-go-1-23-9f7e98db7b39</li>
</ul>
<hr />
<p><strong>感谢阅读！</strong></p>
<p>如果这篇文章让你对后量子密码学和 Go 的未来有所了解，请帮忙<strong>转发</strong> ，让更多 Gopher 关注这一重要趋势！</p>
<hr />
<p><strong>深入探讨，加入我们！</strong></p>
<p>对后量子密码学的技术细节、Go 的具体实现进展、或如何在你的应用中规划 PQC 过渡感兴趣？欢迎加入我的知识星球 <strong>“Go &amp; AI 精进营”</strong>！</p>
<p>在那里，我们可以：</p>
<ul>
<li>跟踪 PQC 在 Go 及其他主流语言中的最新动态。</li>
<li>讨论 PQC API 设计的最佳实践。</li>
<li>分享学习 PQC 和密码学的心得体会。</li>
</ul>
<p>欢迎扫描下方二维码加入星球，共同探索安全技术的未来！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证</title>
		<link>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/</link>
		<comments>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/#comments</comments>
		<pubDate>Sun, 30 Mar 2025 22:05:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AuthorizedKeysCommand]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[bastionzero]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[IdP]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[login]]></category>
		<category><![CDATA[Microsoft]]></category>
		<category><![CDATA[OIDC]]></category>
		<category><![CDATA[OP]]></category>
		<category><![CDATA[OpenIDConnect]]></category>
		<category><![CDATA[openpubkey]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[opkssh]]></category>
		<category><![CDATA[pop]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sshd]]></category>
		<category><![CDATA[sshd_config]]></category>
		<category><![CDATA[SSO]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[私钥]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4532</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/03/31/openpubkey-ssh-open-source 对于许多开发者和运维工程师而言，管理SSH密钥是一项繁琐且易出错的任务。正如SSH发明者、芬兰计算机科学家Tatu Ylonen所指出的，许多组织中过时授权密钥的数量甚至远超员工人数，这带来了巨大的安全隐患。现在，一个基于Go语言生态的创新项目——OpenPubkey SSH (OPKSSH)，旨在彻底改变这一现状。近日，随着Cloudflare将OPKSSH代码捐赠给Linux基金会下的OpenPubkey项目并将其开源，开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式：使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。 1. 核心看点：OPKSSH 开源与价值解读 OPKSSH (OpenPubkey SSH) 是一个巧妙的工具，它将OpenID Connect (OIDC) 等现代SSO技术与SSH协议集成起来，其核心目标是消除手动管理和配置SSH公私钥的需求，同时不引入除身份提供商(IdP)之外的任何新的可信第三方。 此前，虽然底层的OpenPubkey协议已于2023年成为Linux基金会的开源项目，但OPKSSH作为BastionZero（现已被Cloudflare收购）的产品，一直是闭源的。Cloudflare的此次捐赠，使得整个OpenPubkey技术栈的关键应用层实现也完全开放，这对于Go社区和整个基础设施安全领域都是一个重要进展。 2. OPKSSH解决了什么痛点？ 通常，我们在进行远程服务器管理和运维操作时会使用SSH免密登录，即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题： 密钥分发与轮换困难：需要手动将公钥部署到目标服务器，密钥泄露或员工离职后的吊销流程复杂。 长期密钥风险：长期存在的私钥增加了泄露风险，一旦泄露，影响范围广。 可见性差：难以清晰追踪谁拥有对哪些服务器的访问权限，公钥本身缺乏身份信息。 这些问题常常困扰企业的IT运维团队和安全管理人员，他们需要确保访问控制的安全性和可管理性，同时降低操作复杂性和人力成本。 那如何解决这些问题呢？OPKSSH带来了新的解决方案。 3. OPKSSH如何解决这些问题？ OPKSSH基于OpenPubkey协议，带来了革命性的改进： 使用临时性密钥(Ephemeral Keys)提升安全性 OPKSSH使用按需生成的临时SSH密钥对取代长期密钥。用户通过SSO登录后，OPKSSH自动生成有效期较短（默认为24小时，可配置）的密钥。这大大缩短了密钥泄露的风险窗口。 通过单点登录(SSO Login)增强易用性 用户只需运行opkssh login，通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证，即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件，即可在任何安装了opkssh的机器上进行SSH连接。 通过Identity-based Auth提升可见性与简化管理 授权不再基于难以管理的公钥列表（比如~/.ssh/known_hosts），而是基于易于理解和审计的用户身份（如Email地址）。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。 到这里你可能会问：这么好用的OPKSSH是如何工作的呢？别急，我们下面就来介绍一下OPKSSH的工作原理。 4. OPKSSH的工作原理 Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图，这里也借用过来： 如图所示，当用户alice@example.com使用OPKSSH登录服务器，这个过程大致如下： 用户本地执行命令opkssh login触发OIDC流程，用户向IdP认证。 OpenPubkey协议介入，在OIDC流程中巧妙地将用户临时生成的公钥与用户的身份信息绑定，生成一个PK Token(本质上是一个增强的ID Token，包含了公钥信息并由IdP签名)。 OPKSSH将此PK Token打包进一个临时的SSH [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/03/31/openpubkey-ssh-open-source">本文永久链接</a> &#8211; https://tonybai.com/2025/03/31/openpubkey-ssh-open-source</p>
<p>对于许多开发者和运维工程师而言，管理SSH密钥是一项繁琐且易出错的任务。正如<a href="https://ylonen.org/index.html">SSH发明者、芬兰计算机科学家Tatu Ylonen</a>所指出的，许多组织中过时授权密钥的数量甚至远超员工人数，这带来了巨大的安全隐患。现在，一个基于Go语言生态的创新项目——<a href="https://github.com/openpubkey/opkssh">OpenPubkey SSH (OPKSSH)</a>，旨在彻底改变这一现状。近日，随着Cloudflare将<a href="https://github.com/openpubkey/opkssh">OPKSSH代码</a>捐赠给Linux基金会下的<a href="https://github.com/openpubkey">OpenPubkey项目</a>并将其开源，开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式：使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。</p>
<h2>1. 核心看点：OPKSSH 开源与价值解读</h2>
<p>OPKSSH (OpenPubkey SSH) 是一个巧妙的工具，它将<a href="https://tonybai.com/2023/12/22/understand-oidc-by-example/">OpenID Connect (OIDC)</a> 等现代SSO技术与<a href="https://www.ssh.com/academy/ssh">SSH协议</a>集成起来，其核心目标是<strong>消除手动管理和配置SSH公私钥的需求</strong>，同时<strong>不引入除身份提供商(IdP)之外的任何新的可信第三方</strong>。</p>
<p>此前，虽然底层的<a href="https://github.com/openpubkey/openpubkey">OpenPubkey协议已于2023年成为Linux基金会的开源项目</a>，但OPKSSH作为BastionZero（现已被Cloudflare收购）的产品，一直是闭源的。Cloudflare的此次捐赠，使得整个OpenPubkey技术栈的关键应用层实现也完全开放，这对于Go社区和整个基础设施安全领域都是一个重要进展。</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-2.jpeg" alt="" /></p>
<h2>2. OPKSSH解决了什么痛点？</h2>
<p>通常，我们在进行远程服务器管理和运维操作时会使用SSH免密登录，即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题：</p>
<ul>
<li><strong>密钥分发与轮换困难</strong>：需要手动将公钥部署到目标服务器，密钥泄露或员工离职后的吊销流程复杂。</li>
<li><strong>长期密钥风险</strong>：长期存在的私钥增加了泄露风险，一旦泄露，影响范围广。</li>
<li><strong>可见性差</strong>：难以清晰追踪谁拥有对哪些服务器的访问权限，公钥本身缺乏身份信息。</li>
</ul>
<p>这些问题常常困扰企业的IT运维团队和安全管理人员，他们需要确保访问控制的安全性和可管理性，同时降低操作复杂性和人力成本。</p>
<p>那如何解决这些问题呢？OPKSSH带来了新的解决方案。</p>
<h2>3. OPKSSH如何解决这些问题？</h2>
<p>OPKSSH基于<a href="https://www.bastionzero.com/openpubkey-faq">OpenPubkey协议</a>，带来了革命性的改进：</p>
<ul>
<li><strong>使用临时性密钥(Ephemeral Keys)提升安全性</strong></li>
</ul>
<p>OPKSSH使用<strong>按需生成的临时SSH密钥对</strong>取代长期密钥。用户通过SSO登录后，OPKSSH自动生成有效期较短（默认为24小时，可配置）的密钥。这大大缩短了密钥泄露的风险窗口。</p>
<ul>
<li><strong>通过单点登录(SSO Login)增强易用性</strong></li>
</ul>
<p>用户只需运行opkssh login，通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证，即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件，即可在任何安装了opkssh的机器上进行SSH连接。</p>
<ul>
<li><strong>通过Identity-based Auth提升可见性与简化管理</strong></li>
</ul>
<p>授权不再基于难以管理的公钥列表（比如~/.ssh/known_hosts），而是基于易于理解和审计的用户身份（如Email地址）。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。</p>
<p>到这里你可能会问：这么好用的OPKSSH是如何工作的呢？别急，我们下面就来介绍一下OPKSSH的工作原理。</p>
<h2>4. OPKSSH的工作原理</h2>
<p>Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图，这里也借用过来：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-5.png" alt="" /></p>
<p>如图所示，当用户alice@example.com使用OPKSSH登录服务器，这个过程大致如下：</p>
<ul>
<li>用户本地执行命令opkssh login触发OIDC流程，用户向IdP认证。</li>
<li>OpenPubkey协议介入，在OIDC流程中巧妙地将用户<strong>临时生成的公钥</strong>与用户的身份信息绑定，生成一个<strong>PK Token</strong>(本质上是一个增强的ID Token，包含了公钥信息并由IdP签名)。</li>
<li>OPKSSH将此PK Token打包进一个临时的SSH 公钥文件（利用SSH证书的扩展字段）。</li>
<li>当用户发起SSH连接时，这个特殊的公钥文件被发送到服务器。</li>
<li>服务器配置了AuthorizedKeysCommand指令，调用opkssh verify(OpenPubkey验证器)。</li>
<li>验证器检查PK Token的有效性（签名、有效期、颁发者），提取公钥和用户身份(Email)，并根据服务器配置判断该用户是否有权访问。</li>
</ul>
<p>关键在于，这一切<strong>无需修改现有的SSH客户端或服务器软件本身</strong>，仅需在服务器端sshd_config中添加两行配置即可启用，这个我们在本文后面会详细说明。</p>
<p>OPKSSH的魔力源于其底层的<strong>OpenPubkey</strong>协议。OpenPubkey本身是一个基于Go语言实现的Linux基金会项目 (<a href="https://github.com/openpubkey/openpubkey">github.com/openpubkey/openpubkey</a>)。</p>
<p>OpenPubkey的核心创新在于，它通过一种<strong>客户端修改</strong>的方式，将用户持有的公钥(PKu)与OIDC的ID Token进行了加密绑定，而<strong>无需 OIDC 提供商(OP)作任何修改</strong>。这是通过巧妙利用OIDC流程中的nonce参数实现的。客户端不再生成完全随机的nonce，而是生成一个包含其公钥等信息的<strong>客户端实例声明(cic)</strong>，并将cic的哈希值作为nonce发送给OP。OP在签发ID Token时会包含这个nonce。这样，最终得到的PK Token就同时承载了OP 对用户身份的认证以及用户对其公钥的所有权声明（通过客户端的额外签名防止身份误绑定攻击）。</p>
<p>这一机制将OIDC的认证模型从<strong>持有者认证(Bearer Authentication)</strong> 升级到了<strong>持有证明(Proof-of-Possession, PoP)</strong>。在Bearer模型下，任何窃取到ID Token的人都可以冒充用户；而在PoP模型下，用户需要证明自己持有与PK Token中公钥对应的私钥，从而有效抵御<strong>令牌重放(Token Replay)</strong> 和<strong>令牌泄露(Token Export)</strong> 攻击，安全性显著提高。</p>
<p>OpenPubkey的设计还考虑了可扩展性，例如引入MFA-Cosigner概念，可以进一步增强安全性，甚至在OP本身被攻陷的情况下也能提供保护。关于OpenPubkey协议设计的详细内容，可以参见参考资料中OpenPubkey的论文，这里就不赘述了。</p>
<p>了解了原理之后，下面我们来实际验证一下opkssh通过IdP实现SSO一键登录服务器的效果。</p>
<h2>5. 使用opkssh实现免密登录服务器</h2>
<p>这次验证的环境是这样的：</p>
<ul>
<li>客户端：macOS</li>
<li>服务端：Ubuntu 22.04.1 LTS </li>
<li>IdP：microsoft (注：国内访问microsoft的服务器成功率高)</li>
</ul>
<p>我们先来看看客户端的操作步骤：</p>
<h3>5.1 opkssh在客户端的操作</h3>
<p>首先在客户端安装opkssh，你可以选择直接下载编译好的opkssh二进制文件：</p>
<pre><code>$curl -L https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-osx-amd64 -o opkssh; chmod +x opkssh
</code></pre>
<p>由于opkssh是纯Go实现的，如果你本地有Go工具链，也可以选择通过源码安装(在国内，可能选择源码安装的速度更快)：</p>
<pre><code>$go install github.com/openpubkey/opkssh@latest
</code></pre>
<p>安装完成后，我们就来进行客户端的IdP认证。输入下面命令：</p>
<pre><code>$opkssh login
INFO[0000] Opening browser to http://127.0.0.1:59638/chooser
</code></pre>
<p>该命令会打开本地浏览器，并展示下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-3.png" alt="" /></p>
<p>截止到目前，opkssh支持选择Google、Microsoft或Gitlab作为IdP，这里我们选择<strong>Sign in with Microsoft</strong>。</p>
<p>之后浏览器将跳转到下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-4.png" alt="" /></p>
<p>这里使用我的Microsoft账号进行身份认证，点击“接受”，即完成认证，之后你可以关闭页面！</p>
<p>而命令行也会提示下面信息：</p>
<pre><code>INFO[0002] listening on http://127.0.0.1:3000/
INFO[0002] press ctrl+c to stop
Writing opk ssh public key to /Users/tonybai/.ssh/id_ed25519.pub and corresponding secret key to /Users/tonybai/.ssh/id_ed25519Keys generated for identity
Email, sub, issuer, audience:
bigwhite.cn@hotmail.com AAAAAAAAAAAAAAAAAAAAAP5YMhbf2Ufl_eI1PdK12VE https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01
</code></pre>
<p>接下来，我们再来看看服务端要进行的操作与配置。</p>
<h3>5.2 opkssh在服务端的操作</h3>
<p>在要登录的服务器端安装opkssh，由于安装后还要进行一些设置，我建议直接采用opkssh项目提供的安装脚本进行安装：</p>
<pre><code>$ wget -qO- "https://raw.githubusercontent.com/openpubkey/opkssh/main/scripts/install-linux.sh" | sudo bash
Detected OS is debian
Created group: opksshuser
Created user: opksshuser with group: opksshuser
Downloading version latest of opkssh from https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-linux-amd64...
opkssh                           100%[========================================================&gt;]  16.01M  83.4MB/s    in 0.2s
Installed opkssh to /usr/local/bin/opkssh
Configuring opkssh:
  Creating sudoers file at /etc/sudoers.d/opkssh...
  Adding sudoers rule for opksshuser...
Installation successful! Run 'opkssh' to use it.
</code></pre>
<p>之后我们需要修改一下服务端的sshd server的配置。SSH服务器支持一个名为AuthorizedKeysCommand的配置参数，该参数允许我们使用自定义程序来确定SSH公钥是否被授权。因此，我们通过对/etc/ssh/sshd_config文件进行以下两行更改，将SSH服务器的配置文件更改为使用OpenPubkey验证程序而不是SSH默认的验证程序：</p>
<pre><code>AuthorizedKeysCommand /usr/local/bin/opkssh verify %u %k %t
AuthorizedKeysCommandUser opksshuser
</code></pre>
<p>然后通过opkssh添加授权的用户，这些用户登录后将具备root用户权限：</p>
<pre><code>$opkssh add root bigwhite.cn@hotmail.com microsoft
Successfully added new policy to /etc/opk/auth_id
</code></pre>
<p>最后重启一下sshd服务：</p>
<pre><code>$systemctl daemon-reload
$systemctl status sshd
</code></pre>
<h3>5.3 ssh登录验证</h3>
<blockquote>
<p>注：为了避免使用之前的ssh免密登录，可以在服务端将.ssh/authorized_keys中的公钥删除！</p>
</blockquote>
<p>服务端的opkssh命令行被sshd服务调用进行客户端验证时，会在/var/log/opkssh.log中打印相关日志，这也是opkssh起到作用的一个间接证明。</p>
<p>我在客户端依然以原先的ssh登录命令尝试登录服务器：</p>
<pre><code>$ssh root@&lt;your_server_ip&gt;
</code></pre>
<p>我们在服务端opkssh.log中可以看到下面一些输出：</p>
<pre><code>2025/03/29 02:57:43 /usr/local/bin/opkssh verify root AAAAKGVjZHNhLXNoYTItbDUQAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSXO9YZhMPnGkYfnwpFu/HeX29s7q0l4lK5qCgvaeaWh3zBSidDh49Nirsu5Iwh7YVRkKMa5q+hhnJEFAh7FL5LAAAAZAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASQAAACEAqD5msj3BsQhlpszOJHBoIcmK3Ex/BwyNWKHgp6labScAAAAgULO5naYi9xOmzrShcGiVIprRbdSvdWltioSVKu63h6Y= ecdsa-sha2-nistp256-cert-v01@openssh.com
2025/03/29 02:57:43 Providers loaded:  https://accounts.google.com 206584157355-7cbe4s640tvm7naoludob4ut1emii7sf.apps.googleusercontent.com 24h
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01 24h
https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h

2025/03/29 02:57:44 warning: failed to load user policy: failed to read user policy file /root/.opk/auth_id: error reading root home policy using command /usr/bin/sudo -n /usr/local/bin/opkssh readhome root got output Failed to read user's home policy file: failed to open /root/.opk/auth_id, open /root/.opk/auth_id: no such file or directory
 and err exit status 1
2025/03/29 02:57:44 successfully verified
</code></pre>
<p>之后，我就成功登录到服务器上了！</p>
<h2>6.小结</h2>
<p>OPKSSH 的开源是 OpenPubkey 项目和 Go 安全生态的重要里程碑。它不仅提供了一个解决 SSH 密钥管理难题的实用方案，也展示了 Go 语言在构建安全、可靠的基础设施工具方面的强大能力。</p>
<p>我们鼓励对安全、身份认证和 Go 开发感兴趣的开发者们：</p>
<ul>
<li><strong>试用 OPKSSH</strong>: 在你的开发或测试环境中体验 SSO 登录 SSH 的便捷。</li>
<li><strong>关注 OpenPubkey 项目</strong>: Star GitHub 仓库，了解最新动态。</li>
<li><strong>参与社区贡献</strong>: 通过 Pull Request、Issue 反馈、参与讨论等方式为项目贡献力量。可以在 OpenSSF Slack 的 <code>#openpubkey</code> 频道找到社区成员，或参加每月一次的社区会议。</li>
</ul>
<p>随着 OPKSSH 的加入和持续发展，我们期待 OpenPubkey 能够在更多场景下发挥价值，例如代码签名 (Sigstore 集成)、端到端加密通信等，进一步丰富和巩固 Go 语言在云原生和安全领域的基础设施地位。</p>
<h2>7. 参考资料</h2>
<ul>
<li><a href="https://github.com/openpubkey/opkssh/">OPKSSH项目</a> &#8211; https://github.com/openpubkey/opkssh</li>
<li><a href="https://eprint.iacr.org/2023/296">Paper: OpenPubkey: Augmenting OpenID Connect with User held Signing Keys</a> &#8211; https://eprint.iacr.org/2023/296</li>
<li><a href="https://github.com/openpubkey/openpubkey/">OpenPubkey项目</a> &#8211; https://github.com/openpubkey/openpubkey/</li>
<li><a href="https://blog.cloudflare.com/open-sourcing-openpubkey-ssh-opkssh-integrating-single-sign-on-with-ssh">Open-sourcing OpenPubkey SSH (OPKSSH): integrating single sign-on with SSH</a> &#8211; https://blog.cloudflare.com/open-sourcing-openpubkey-ssh-opkssh-integrating-single-sign-on-with-ssh</li>
<li><a href="https://www.docker.com/blog/how-to-use-openpubkey-to-solve-key-management-via-sso">How to Use OpenPubkey to Solve Key Management via SSO</a> &#8211; https://www.docker.com/blog/how-to-use-openpubkey-to-solve-key-management-via-sso/</li>
<li><a href="https://www.bastionzero.com/openpubkey-faq">Open Pubkey Frequently Asked Questions</a> &#8211; https://www.bastionzero.com/openpubkey-faq</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.24新特性前瞻：工具链和标准库</title>
		<link>https://tonybai.com/2024/12/17/go-1-24-foresight-part2/</link>
		<comments>https://tonybai.com/2024/12/17/go-1-24-foresight-part2/#comments</comments>
		<pubDate>Mon, 16 Dec 2024 21:58:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AddCleanup]]></category>
		<category><![CDATA[aliastypeparams]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[clear]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[filepath]]></category>
		<category><![CDATA[FIPS]]></category>
		<category><![CDATA[futex]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-playground]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[GOAUTH]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[GOCACHEPROG]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golangci-lint]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[gotip]]></category>
		<category><![CDATA[GoWiki]]></category>
		<category><![CDATA[HashTrieMap]]></category>
		<category><![CDATA[hkdf]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[lock]]></category>
		<category><![CDATA[loop]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[max]]></category>
		<category><![CDATA[min]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[nocallback]]></category>
		<category><![CDATA[noescape]]></category>
		<category><![CDATA[omitzero]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[PBKDF2]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[pseudo-version]]></category>
		<category><![CDATA[RAII]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Scaling]]></category>
		<category><![CDATA[SetFinalizer]]></category>
		<category><![CDATA[sha3]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[spinbit]]></category>
		<category><![CDATA[spinning]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[stringer]]></category>
		<category><![CDATA[swiss-table]]></category>
		<category><![CDATA[synctest]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[Thread]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[tool]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[typealias]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[wasmexport]]></category>
		<category><![CDATA[weak]]></category>
		<category><![CDATA[伪版本号]]></category>
		<category><![CDATA[原子操作]]></category>
		<category><![CDATA[后量子密码]]></category>
		<category><![CDATA[实验特性]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[类型别名]]></category>
		<category><![CDATA[线程]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[逃逸]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4442</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/17/go-1-24-foresight-part2 在上一篇文章中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。 1. 工具链 1.1 go.mod新增tool指示符，支持对tool的依赖管理(#48429) 我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具，比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具，以避免在不同时间、不同环境中的输出不同的结果。因此，Go社区希望通过go.mod将工具的版本以及依赖管理起来。 在Go 1.24版本之前，Go Wiki推荐tools.go的一种来自社区的最佳实践，阐述这种实践的最好的一个示例来自Go modules by example中的一个文档：”Tools as dependencies“，其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中，以golang.org/x/tools/cmd/stringer为例，tools.go的内容大致如下： //go:build tools package tools import ( _ "golang.org/x/tools/cmd/stringer" ) 然后在同一目录下安装stringer或直接go run： $go install golang.org/x/tools/cmd/stringer 在安装stringer时，go.mod会记录下对stringer的依赖以及对应的版本，后续go.mod提交到项目repo中，所有项目成员就都可以使用相同版本的Stringer了。 tools.go实践虽然能解决问题，但这种方式还是存在一些不便： 配置繁琐：需要手动创建 tools.go 文件，并添加特定的构建标签来排除它； 使用不便：运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。 Go开发者期望工具依赖也能够无缝地与其他项目依赖(包依赖)统一管理，并纳入go.mod的版本控制体系。 为此，该提案设计并实现了下面几点以满足开发者的上述述求： go.mod引入tool directive，用于显式声明项目所需的工具。 tool directive与其他依赖项统一纳入go.mod文件，方便管理和版本控制。 扩展go install和go get命令，支持安装、更新和卸载工具。 我们来看一个示例，首先我们初始化一个module： $ gotip mod [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-24-foresight-part2-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/17/go-1-24-foresight-part2">本文永久链接</a> &#8211; https://tonybai.com/2024/12/17/go-1-24-foresight-part2</p>
<p>在<a href="https://tonybai.com/2024/12/16/go-1-24-foresight-part1/">上一篇文章</a>中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。</p>
<h2>1. 工具链</h2>
<h3>1.1 <a href="https://github.com/golang/go/issues/48429">go.mod新增tool指示符，支持对tool的依赖管理(#48429)</a></h3>
<p>我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具，比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具，以避免在不同时间、不同环境中的输出不同的结果。因此，Go社区希望通过go.mod将工具的版本以及依赖管理起来。</p>
<p>在Go 1.24版本之前，Go Wiki推荐tools.go的一种来自社区的最佳实践，阐述这种实践的最好的一个示例来自Go modules by example中的一个文档：”<a href="https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md">Tools as dependencies</a>“，其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中，以golang.org/x/tools/cmd/stringer为例，tools.go的内容大致如下：</p>
<pre><code>//go:build tools

package tools

import (
    _ "golang.org/x/tools/cmd/stringer"
)
</code></pre>
<p>然后在同一目录下安装stringer或直接go run：</p>
<pre><code>$go install golang.org/x/tools/cmd/stringer
</code></pre>
<p>在安装stringer时，go.mod会记录下对stringer的依赖以及对应的版本，后续go.mod提交到项目repo中，所有项目成员就都可以使用相同版本的Stringer了。</p>
<p>tools.go实践虽然能解决问题，但这种方式还是存在一些不便：</p>
<ul>
<li>配置繁琐：需要手动创建 tools.go 文件，并添加特定的<a href="https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package">构建标签</a>来排除它；</li>
<li>使用不便：运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。</li>
</ul>
<p>Go开发者期望<strong>工具依赖</strong>也能够无缝地与其他项目依赖(包依赖)统一管理，并纳入go.mod的版本控制体系。</p>
<p>为此，该提案设计并实现了下面几点以满足开发者的上述述求：</p>
<ul>
<li>go.mod引入tool directive，用于显式声明项目所需的工具。</li>
<li>tool directive与其他依赖项统一纳入go.mod文件，方便管理和版本控制。</li>
<li>扩展go install和go get命令，支持安装、更新和卸载工具。</li>
</ul>
<p>我们来看一个示例，首先我们初始化一个module：</p>
<pre><code>$ gotip mod init demo
go: creating new go.mod: module demo
$ cat go.mod
module demo

go 1.24
</code></pre>
<p>编辑go.mod，加入下面内容：</p>
<pre><code>$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer
</code></pre>
<p>安装tool前需要go get它的依赖，否则go install会报错：</p>
<pre><code>$gotip install tool
no required module provides package golang.org/x/tools/cmd/stringer; to add it:
    go get golang.org/x/tools/cmd/stringer

$gotip get golang.org/x/tools/cmd/stringer
go: downloading golang.org/x/tools v0.28.0
go: downloading golang.org/x/sync v0.10.0
go: downloading golang.org/x/mod v0.22.0
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)
</code></pre>
<p>我们看到：go.mod中require了stringer的依赖。</p>
<p>接下来，我们便可以用go install安装stringer了：</p>
<pre><code>$ ls -l `which stringer` // old版本的stringer
-rwxr-xr-x 1 root root 6500561 1月  23 2024 /root/go/bin/stringer

$ gotip install tool
$ ls -l `which stringer`
-rwxr-xr-x 1 root root 7303970 12月  9 21:41 /root/go/bin/stringer
</code></pre>
<p>后续要更新stringer版本，可以直接使用go get -u：</p>
<pre><code>$gotip get -u golang.org/x/tools/cmd/stringer
</code></pre>
<p>此外，除了手工编辑go.mod，添加依赖的tool外，我们也可以直接使用go get -tool像go.mod中添加依赖的tool，它们在效果上是等价的：</p>
<pre><code>// 重置go.mod到最初状态
# cat go.mod
module demo

go 1.24

// 执行go get -tool
$gotip get -tool golang.org/x/tools/cmd/stringer
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)
</code></pre>
<p>使用stringer时也无需手工敲入那么长的命令(go run golang.org/x/tools/cmd/stringer)，只需使用gotip tool stringer即可：</p>
<pre><code>$ gotip tool stringer
Usage of stringer:
    stringer [flags] -type T [directory]
    stringer [flags] -type T files... # Must be a single package
For more information, see:

https://pkg.go.dev/golang.org/x/tools/cmd/stringer

Flags:
  -linecomment
        use line comment text as printed text when present
  -output string
        output file name; default srcdir/&lt;type&gt;_string.go
  -tags string
        comma-separated list of build tags to apply
  -trimprefix prefix
        trim the prefix from the generated constant names
  -type string
        comma-separated list of type names; must be set
</code></pre>
<p>go tool stringer就相当于go run golang.org/x/tools/cmd/stringer@v0.28.0了(注：v0.28.0是当前golang.org/x/tools的版本)。</p>
<p>tool directive和go工具链做了很好的融合，除了上面的命令外，还支持：</p>
<ul>
<li>go build tool构建module依赖的tool，并将构建出可执行文件放在当前目录下；</li>
<li>go build -o bin/ tool将构建module依赖的tool，并将构建出可执行文件放在项目自己的bin目录下。</li>
</ul>
<p>到这里，屏幕前的你可能会问一个问题：如果本地多个项目依赖同一个工具的不同版本，比如golangci-lint的v1.62.2和v1.62.0时，那么两个项目安装的golangci-lint是否会相互覆盖和影响呢？我们来验证一下，下面建立两个项目：tool-directive1和tool-directive2。</p>
<pre><code>.
├── tool-directive1/
│   ├── go.mod
│   └── go.sum
└── tool-directive2/
    ├── go.mod
    └── go.sum
</code></pre>
<p>我们先在tool-directive1下面执行下面命令添加对golangci-lint的依赖：</p>
<pre><code>$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
go: downloading github.com/golangci/golangci-lint v1.62.2
go: downloading github.com/gofrs/flock v0.12.1
go: downloading github.com/fatih/color v1.18.0
... ...
</code></pre>
<p>然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：</p>
<pre><code>$ gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)
</code></pre>
<p>我们看到tool-directive1依赖了v1.62.2版本的golangci-lint。不过你在执行上述命令时可能会注意到，这个命令的执行非常耗时，可能需要10~20s才能出结果。如果你再执行一次，它就可以瞬间输出结果，为什么会这样的？稍后我们给出答案。</p>
<p>现在我们切换到tool-directive2目录下，执行下面命令添加对golangci-lint v1.62.0版本的依赖：</p>
<pre><code>$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0
</code></pre>
<p>然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：</p>
<pre><code>$gotip tool golangci-lint --version
golangci-lint has version v1.62.0 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI=") on (unknown)
</code></pre>
<p>我们看到tool-directive2下得到的是v1.62.0版本的golangci-lint。并且我们会遇到同样的现象：第一次执行很慢，第二次执行就会瞬间出结果。</p>
<p>再回到tool-directive1下，看看它依赖的golangci-lint是否被覆盖了：</p>
<pre><code>$gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)
</code></pre>
<p>我们发现：两个项目下依赖的版本各自独立，并不会相互覆盖。</p>
<p>这其中的缘由又是什么呢？为什么使用go tool golangci-lint第一次执行会慢，而后续的执行就会飞快呢？下面的issue将回答这个问题。</p>
<h3>1.2 <a href="https://github.com/golang/go/issues/69290">Go run生成的可执行文件支持缓存(#69290)</a></h3>
<p>Go 1.24 之前，cmd/go仅缓存编译后的包文件（build actions），而不缓存链接后的二进制文件（link actions）。不缓存二进制文件很大原因在于二进制文件比单个包对象文件大得多，并且它们不像包文件那样被经常重用。</p>
<p>不过上述1.1中，让go支持对依赖工具的管理以及让go tool支持自定义工具执行的issue让这个issue最终被纳入Go 1.24。该issue实现后，go run以及像上面那种go tool golangci-lint(本质上也是go run github.com/golangci/golangci-lint/cmd/golangci-lint@vx.y.z)的编译链接的结果会被缓存到go build cache中。这也是上面不同项目依赖同一工具不同版本时不会相互覆盖以及首次使用go tool执行依赖工具较慢的原因，第一次go tool执行会执行编译链接过程，之后的运行就会从缓存中直接找到缓存的文件并执行了。</p>
<p>由于这个issue会显著增大go build cache的磁盘空间占用，该issue也规定了，在<a href="https://github.com/golang/go/issues/68872">缓存执行定期清理</a>的时候，<strong>可执行文件缓存会优先于包缓存被优先清理掉</strong>。</p>
<h3>1.3 <a href="https://github.com/golang/go/issues/50603">Go build支持生成伪版本号(#50603)</a></h3>
<p>在Go 1.18及之后的版本中，cmd/go工具链在构建二进制文件时会嵌入依赖版本信息和VCS（版本控制系统）信息，这使得开发者可以更容易地追踪二进制文件的来源。然而，当使用go build命令构建主模块时，主模块的版本信息并不会被记录，而是显示为(devel)，这导致开发者需要使用外部构建脚本或-ldflags来手动设置版本信息。相比之下，go install命令会正确记录主模块的版本信息。</p>
<p>该issue就旨在让go build命令也能像go install一样，自动嵌入主模块的版本信息，从而避免开发者依赖外部构建脚本。</p>
<p>落地后，Go 1.24的go build命令会在编译后的二进制文件中包含版本信息。如果本地VCS（版本控制系统）标签可用，主模块的版本将从该标签中设置。如果没有本地VCS标签可用，则会生成一个伪版本（pseudo-version），通常包含时间戳和提交哈希。 此外，为了避免与已发布的版本混淆，go build还会在伪版本中添加一些特殊的标识符，例如devel，以表明这是一个本地构建的版本。如果有未提交的VCS更改，则会附加一个+dirty后缀。</p>
<p>使用-buildvcs=false标志可以省略二进制文件中的版本控制信息。</p>
<p>下面对比一下Go 1.24版本之前与Go 1.24版本在go build时生成的版本信息的差异：</p>
<p>以Go 1.23为例，其构建和安装的stringer的版本信息如下：</p>
<pre><code>$go version  -m `which stringer`
/root/go/bin/stringer: go1.23.0
... ...
</code></pre>
<p>而使用go1.24的build构建的stringer的版本信息如下：</p>
<pre><code>$go version  -m tool-directive1/bin/stringer
tool-directive1/bin/stringer: devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000
... ...
</code></pre>
<h3>1.4 <a href="https://github.com/golang/go/issues/64876">默认使能GOCACHEPROG以支持外部缓存</a></h3>
<p>估计Go社区很少有人用过GOCACHEPROG，即便在Go 1.21版本之后，它是以实验特性的形式提供的，通过GOEXPERIMENT=cacheprog启用。这个特性是由Go语言元老<a href="https://github.com/bradfitz">Brad Fitzpatrick</a>提出的，其主issue编号是<a href="https://github.com/golang/go/issues/59719">59719</a>。</p>
<p>我们知道：Go语言的cmd/go工具已经具备了强大的缓存支持，但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高，尤其是在CI（持续集成）环境中，用户通常需要将GOCACHE目录打包和解压缩，这往往比CI操作本身还要慢。此外，用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率，但这些功能并不适合直接集成到cmd/go工具中。</p>
<p>为了解决上述问题，Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG，类似于现有的GOCACHE变量。通过设置GOCACHEPROG，用户可以指定一个外部程序，该程序将作为子进程运行，并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口<strong>与外部缓存程序交互</strong>，外部程序可以根据需要实现任意的缓存机制和策略。</p>
<p>为此，Bradfitz在issue 59719中给出了交互的协议设计。cmd/go工具与外部缓存程序之间的通信基于JSON格式的消息。消息分为请求（ProgRequest）和响应（ProgResponse）。请求包括命令类型、操作ID（ActionID）、对象ID（ObjectID）等。响应则包括缓存命中与否、对象的磁盘路径等信息。</p>
<p>其中请求的命令类型有如下几种：</p>
<ul>
<li>get：从缓存中获取对象。</li>
<li>put：将对象存入缓存。</li>
<li>close：关闭缓存连接。</li>
</ul>
<p>对于put请求，cmd/go工具会将对象的二进制数据通过base64编码后发送给外部程序。对于get请求，外部程序返回对象的磁盘路径。</p>
<p>在\$GOROOT/src/cmd/go/internal/cache/prog.go文件中可以看到具体协议相关的结构。</p>
<p>Bradfitz还给出了一个<a href="https://github.com/bradfitz/go-tool-cache">外部cache的样例程序go-tool-cache</a>，还有开发者fork了该样例程序，将它改造为<a href="https://github.com/or-shachar/go-tool-cache/">以S3为后端cache的外部缓存程序</a>。感兴趣的童鞋，可以按照这些样例程序的说明试验一下外部缓存功能。</p>
<h3>1.5 <a href="https://github.com/golang/go/issues/26232">go工具链支持HTTP扩展认证：GOAUTH(#26232)</a></h3>
<p>在Go语言中，go get命令用于从远程代码仓库获取依赖包。通常，这些依赖包的导入路径是通过HTTP请求获取的，服务器会返回一个包含元标签（meta tag）的HTML页面，指示如何获取该包的源代码。然而，对于需要身份验证的私有仓库，go get无法直接工作，因为go get使用的是net/http.DefaultClient，它不知道如何处理需要身份验证的URL。具体来说，当go get尝试获取一个私有仓库的URL时，由于没有提供身份验证信息，服务器会返回401或403错误，导致go get无法继续执行。这个问题在企业环境中尤为常见，因为许多公司使用私有代码托管服务，而这些服务通常需要身份验证。</p>
<p>issue 26232为上述情况提供了一种方案，让go get能够支持需要身份验证的私有仓库，使得用户可以通过go get命令获取私有仓库中的代码：</p>
<pre><code>$go get git.mycompany.com/private-repo
</code></pre>
<p>即使https://git.mycompany.com/private-repo需要身份验证，go get也能够正常工作。</p>
<p>方案采用了一种类似于Git凭证助手的机制，并通过新增的Go环境变量GOAUTH来指定一个或多个认证命令。go get在执行时会调用这些命令，获取身份验证信息，并在后续的HTTP请求中使用这些信息。</p>
<p>GOAUTH环境变量可以包含一个或多个认证命令，每个命令由空格分隔的参数列表组成，命令之间用分号分隔。go get会在每次需要进行HTTP请求时，首先检查缓存中的认证信息，如果没有匹配的认证信息，则会调用GOAUTH命令来获取新的认证信息。</p>
<p>通过go help goauth可以查看GOAUTH的详细用法，在Go 1.24中它支持如下认证命令：</p>
<ul>
<li>off：禁用GOAUTH功能</li>
<li>netrc：从NETRC或用户主目录中的.netrc文件中获取访问凭证，这也是<strong>GOAUTH的默认值</strong>。</li>
<li>git dir：在指定目录dir中运行git credential fill并使用其凭证。go命令将运行git credential approve/reject来更新凭证助手的缓存。</li>
<li>command：执行给定的命令（以空格分隔的参数列表），并将提供的头信息附加到 HTTPS 请求中。该命令必须按照以下格式生成输出：</li>
</ul>
<pre><code>Response      = { CredentialSet } .
CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine .
URLLine       = /* URL that starts with "https://" */ '\n' .
HeaderLine    = /* HTTP Request header */ '\n' .
BlankLine     = '\n' .
</code></pre>
<h3>1.6 <a href="https://github.com/golang/go/issues/62067">go build支持-json(#62067)</a></h3>
<p>Go 1.24版本之前，Go已经支持了go test -json命令，旨在为测试过程提供结构化的JSON输出，便于工具解析和处理测试结果。然而，当测试或导入的包在构建过程中失败时，构建错误信息会与测试的JSON输出交织在一起，导致工具难以准确地将构建错误与受影响的测试包关联起来。这增加了工具处理go test -json输出的复杂性。</p>
<p>为了解决这个问题，issue 62067提出了为go build命令(包括go install)添加-json标志的建议，以便生成与go test -json兼容的结构化JSON输出。go test -json也得到了优化，现在在test时出现构建错误时，go test -json也会以json格式输出构建错误信息，与test结果的json内容可以很好的融合在一起。当然，你也可以通过GODEBUG=gotestjsonbuildtext=1继续让go test -json输出文本格式的构建错误信息，以保持与Go 1.24之前的情况一致。</p>
<h2>2. 标准库</h2>
<p>Go标准库向来是添加新特性的大户，不过鉴于变化太多，下面我们仅列举一些主要的变化点。</p>
<h3>2.1 <a href="https://github.com/golang/go/issues/45669">json包支持omitzero选项</a></h3>
<p>关于这个变化点，我在《<a href="https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/">JSON包新提案：用“omitzero”解决编码中的空值困局</a>》一文中有详细说明，请移步阅读，这里不赘述了。</p>
<h3>2.2 <a href="https://github.com/golang/go/issues/67552">新增weak包和weak指针</a></h3>
<p>weak包和weak指针是Go团队在设计和实现unique包时的“副产物”，Go团队认为weak指针可以给大家带来更灵活的内存管理机制，于是将其从internal中提到标准库中。我之前的《<a href="https://tonybai.com/2024/09/23/go-weak-package-preview/">Go weak包前瞻：弱指针为内存管理带来新选择</a>》一文对weak包有详细说明，请移步阅读。</p>
<h3>2.3 crypto: FIPS 140-3认证</h3>
<p>在Go 1.24开发周期中，Go密码学小组与Russ Cox根据开发者日益增多的密码学合规性(满足FIPS 140)的需求反馈，决定对Go的加密库进行改造，以符合申请进行FIPS 140标准认证的要求。有关这个认证的issue和改动点(cl)都很多，大家可以阅读我的《<a href="https://tonybai.com/2024/11/16/go-crypto-and-fips-140/">走向合规：Go加密库对FIPS 140的支持</a>》一文了解详情。</p>
<h3>2.4 crypto：增加hkdf、pbkdf2、sha3等密码学包</h3>
<p>读过我的《<a href="https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive">Go开发者的密码学导航：crypto库使用指南</a>》一文的读者都知道：Go密码学团队维护的密码学包分布在Go标准库crypto目录和golang.org/x/crypto下面。Go密码学小组负责人<a href="https://github.com/golang/go/issues/65269">Roland Shoemaker认为当前这种”分割”的状态会带来一些问题</a>：</p>
<ul>
<li>用户困惑：用户经常对为什么某些加密库在x/crypto模块中，而另一些在标准库中感到困惑。这种困惑可能导致用户不愿意依赖x/crypto模块中的代码，因为他们误以为x/crypto中的代码是“实验性”的，质量或API稳定性不如标准库。</li>
<li>复杂的安全补丁流程：标准库依赖于x/crypto模块中的多个包（目前有7个），这些包需要被vendored。这种依赖关系增加了安全补丁的复杂性，因为需要一个特殊的第三方流程来处理这些包的补丁，而不是像标准库或x/crypto模块那样直接处理。</li>
<li>开发周期不一致：理论上，x/crypto模块是一个可以快速开发新加密算法或协议的地方，因为这些算法或协议的规范可能还在变化中。然而，实际上，x/crypto模块并没有被这样使用。如果开始这样做，反而会强化用户对x/crypto模块的误解。</li>
<li>特定包的快速开发需求：例如x/crypto/ssh包最近经历了非常快速的开发，许多用户希望立即使用新引入的功能和修复。如果将这个包移入标准库，可能会因为标准库的发布周期较慢而产生摩擦。</li>
</ul>
<p>为此Shoemaker提议了一个将x/crypto下的包到标准库crypto目录下的方案，以简化Go语言加密库的管理和维护，提高用户对这些库的信任和使用率，方案的大致思路和步骤如下：</p>
<ul>
<li>将x/crypto模块中的大部分包直接迁移到标准库的crypto/目录下，迁移过程应在单个标准库发布周期内完成，尽量接近发布周期的末尾，以避免需要同步两个版本的包。</li>
<li>迁移后，冻结x/crypto模块和标准库中的对应包，直到标准库重新开放，只接受标准库版本的更改。</li>
<li>使用构建标签（build tags）来区分迁移前后的版本，允许用户在不更新到最新Go版本的情况下继续使用x/crypto模块。</li>
<li>在迁移后的两个主要版本中（例如，假设在Go 1.24中完成迁移，则在Go 1.26中），移除旧的构建标签实现，只保留转发到标准库版本的包装器。</li>
<li>一些包由于其更新周期与标准库不一致，或者已经冻结/弃用，将不会迁移到标准库中。例如，x/crypto/x509roots包需要根据任意时间表进行更新，因此应移至独立的模块golang.org/x/x509roots。</li>
<li>一些已经弃用或冻结的包（如twofish、cast5、tea等）将保留在x/crypto模块中，并在v1版本中标记为冻结。</li>
<li>x/crypto/ssh包由于其快速的开发周期，可能会在迁移时带来一些麻烦。虽然可以考虑将其推迟迁移，但最终仍建议将其移入标准库。</li>
</ul>
<p>基于上述方案，Go 1.24版本中，Go密码学团队完成了hkdf、pbkdf2、sha3和mlkem等包的迁移。当然这次迁移与<a href="https://github.com/golang/go/issues/69536">Go密码学包要进行FIPS 140-3认证</a>也有着直接的联系。</p>
<p>这里面值得一提的是mklem包，它实现了<a href="https://doi.org/10.6028/NIST.FIPS.203">NIST FIPS 203</a>中指定的抗量子密钥封装方法ML-KEM（以前称为Kyber），也是Go密码学包中第一个<a href="https://en.wikipedia.org/wiki/Post-quantum_cryptography">后量子密码学</a>包。</p>
<h3>2.5 <a href="https://github.com/golang/go/issues/67002">支持限制目录的文件系统访问(#67002)</a></h3>
<p>目录遍历漏洞（Directory Traversal Vulnerabilities）和符号链接遍历漏洞（Symlink Traversal Vulnerabilities）是常见的安全漏洞。攻击者通过提供相对路径（如”../../../etc/passwd”）或创建符号链接，诱使程序访问其本不应访问的文件，从而导致安全问题。例如，<a href="https://nvd.nist.gov/vuln/detail/CVE-2024-3400">CVE-2024-3400 </a>是一个最近的真实案例，展示了目录遍历漏洞如何导致远程代码执行。</p>
<p>在Go中，虽然可以通过 filepath.IsLocal等函数来验证文件名，但防御符号链接遍历攻击较为困难。现有的os.Open和os.Create等函数在处理不受信任的文件名时，容易受到这些攻击的影响。</p>
<p>为了解决这些问题，issue 67002提出了在os包中添加几个新的函数和方法，以安全地打开文件并防止目录遍历和符号链接遍历攻击。</p>
<p>最初该提案提出新增一些安全访问文件系统的API函数，在讨论过程中，Russ Cox 提出了一个更为简洁的方案，避免了引入大量新的 API，而是通过引入一个新的类型 Dir 来表示受限的文件系统根目录。这个方案最终奠定了该提案的最终实现。</p>
<p>最终Go在os包中引入了一个新的Root类型，并基于该类型提供了在特定目录内执行文件系统操作的能力。os.OpenRoot函数打开一个目录并返回一个os.Root。os.Root上的方法仅限于在该目录内操作，并且不允许路径引用目录外的位置，包括跟随符号链接指向目录外的路径。下面是一些Root类型的常用方法：</p>
<ul>
<li>os.Root.Open 打开一个文件以供读取。</li>
<li>os.Root.Create 创建一个文件。</li>
<li>os.Root.OpenFile 是通用的打开调用。</li>
<li>os.Root.Mkdir 创建一个目录。</li>
</ul>
<p>下面我们用一个示例对比一下通过os.Root进行的文件系统操作与传统文件系统操作的差异：</p>
<pre><code>// go1.24-foresight/stdlib/osroot/main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    // 使用 os.Root 访问相对路径
    root, err := os.OpenRoot(".") // 打开当前目录作为根目录
    if err != nil {
        fmt.Println("Error opening root:", err)
        return
    }
    defer root.Close()

    // 尝试访问相对路径 "../passwd"
    file, err := root.Open("../passwd")
    if err != nil {
        fmt.Println("Error opening file with os.Root:", err)
    } else {
        fmt.Println("Successfully opened file with os.Root")
        file.Close()
    }

    // 传统的 os.OpenFile 方式
    // 尝试访问相对路径 "../passwd"
    file2, err := os.OpenFile("../passwd", os.O_RDONLY, 0644)
    if err != nil {
        fmt.Println("Error opening file with os.OpenFile:", err)
    } else {
        fmt.Println("Successfully opened file with os.OpenFile")
        file2.Close()
    }
}
</code></pre>
<p>运行上述代码，我们得到：</p>
<pre><code>$gotip run main.go
Error opening file with os.Root: openat ../passwd: path escapes from parent
Successfully opened file with os.OpenFile
</code></pre>
<p>我们看到：当代码通过os.Root返回的目录来尝试访问相对路径”../passwd”时，由于os.Root限制了操作仅限于根目录内，因此会返回错误。</p>
<p>从安全角度来看，Go 1.24之后，建议搭建多多使用这种安全操作文件系统的方式，如果你的文件操作都局限在一个目录下。</p>
<h3>2.6 <a href="https://github.com/golang/go/issues/67535">使用runtime.AddCleanup替代SetFinalizer(#67535)</a></h3>
<p>Go 1.24版本之前，Go提供了runtime.SetFinalizer函数用于对象的终结处理。然而，SetFinalizer的使用存在许多问题和限制，Michael Knyszek总结了下面几点：</p>
<ul>
<li>必须引用分配的第一个字：SetFinalizer必须引用分配的第一个字，这要求程序员了解什么是“分配”，而这一概念在语言中通常不暴露。</li>
<li>每个对象只能有一个终结器：不能为同一个对象设置多个终结器。</li>
<li>引用循环问题：如果对象参与了引用循环，且该对象有终结器，那么该对象将不会被释放，终结器也不会运行。</li>
<li>GC周期问题：有终结器的对象至少需要两个GC周期才能被释放。</li>
</ul>
<p>后面两个问题主要源于SetFinalizer允许对象复活（object resurrection），这使得对象的清理变得复杂且不可靠。</p>
<p>为了解决上述问题，，Michael Knyszek提出了一个新的API runtime.AddCleanup，并建议正式弃用runtime.SetFinalizer。AddCleanup的设计目标是解决SetFinalizer的诸多问题，特别是避免对象复活，从而允许对象的及时清理，并支持对象的循环清理。</p>
<p>AddCleanup函数的原型如下：</p>
<pre><code>func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
</code></pre>
<p>AddCleanup函数将一个清理函数附加到ptr。当ptr不再可达时，运行时会在一个单独的goroutine中调用 cleanup(arg)。</p>
<p>AddCleanup的一个典型的用法如下：</p>
<pre><code>f, _ := Open(...)
runtime.AddCleanup(f, func(fd uintptr) { syscall.Close(fd) }, f.Fd())
</code></pre>
<p>通常，ptr是一个包装底层资源的对象（例如上面典型用法中的那个包装操作系统文件描述符的File对象），arg是底层资源（例如操作系统文件描述符），而清理函数释放底层资源（例如，通过调用close系统调用）。</p>
<p>AddCleanup对ptr的约束很少，支持为同一个指针附加多个清理函数。不过，如果ptr可以从cleanup或arg中可达，ptr将永远不会被回收，清理函数也永远不会运行。作为一种简单的保护措施，如果arg等于ptr，AddCleanup会引发panic。清理函数的运行顺序没有指定。特别是，如果几个对象相互指向并且同时变得不可达，它们的清理函数都可以运行，并且可以以任何顺序运行。即使对象形成一个循环也是如此。</p>
<p>cleanup(arg)调用并不总是保证运行，特别是它不保证在程序退出之前能运行。</p>
<p>清理函数可能在对象变得不可达时立即运行。为了正确使用清理函数，程序必须确保对象在清理函数安全运行之前保持可达。存储在全局变量中的对象，或者可以通过从全局变量跟踪指针找到的对象，是可达的。函数参数或方法接收者可能在函数最后一次提到它的地方变得不可达。为了确保清理函数不会过早调用，我们可以将对象传递给KeepAlive函数，以保证对象在保持可达的最后一个点之后依然可达。</p>
<p>到这里，也许一些读者想到了RAII(Resource Acquisition Is Initialization），RAII的核心思想是将资源的获取和释放与对象的生命周期绑定在一起，从而确保资源在对象不再使用时能够被正确释放。似乎AddCleanup可以用于实现Go版本的RAII，下面是一个示例：</p>
<pre><code>// go1.24-foresight/stdlib/addcleanup/main.go

package main

import (
    "fmt"
    "os"
    "runtime"
    "syscall"
    "time"
)

type FileResource struct {
    file *os.File
}

func NewFileResource(filename string) (*FileResource, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    // 使用 AddCleanup 注册清理函数
    fd := file.Fd()
    runtime.AddCleanup(file, func(fd uintptr) {
        fmt.Println("Closing file descriptor:", fd)
        syscall.Close(int(fd))
    }, fd)

    return &amp;FileResource{file: file}, nil
}

func main() {
    fileResource, err := NewFileResource("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }

    // 模拟使用 fileResource
    _ = fileResource
    fmt.Println("File opened successfully")

    // 当 fileResource 不再被引用时，AddCleanup 会自动关闭文件
    fileResource = nil
    runtime.GC() // 强制触发 GC，以便清理 fileResource
    time.Sleep(time.Second * 5)
}
</code></pre>
<p>运行上述代码得到如下结果：</p>
<pre><code>$gotip run main.go
File opened successfully
Closing file descriptor: 3
</code></pre>
<p>的确，在Go中，runtime.AddCleanup可以用来模拟RAII机制，但与传统的RAII有一些不同，在Go中，资源获取通常是通过显式的函数调用来完成的，例如打开文件等，而不是像C++那样在构造函数中隐式完成。并且，资源的释放由Go GC回收对象时触发。如果要实现C++那样的RAII，需要我们自行做一些封装。</p>
<h3>2.7 <a href="https://github.com/golang/go/issues/61515">不易出错的新Benchmark函数(#61515)</a></h3>
<p>在Go语言中，基准测试（benchmarking）是通过testing.B类型的b.N来实现的。b.N表示基准测试需要执行的迭代次数。然而，这种设计存在一些问题：</p>
<ul>
<li>容易忘记使用b.N：在某些情况下，开发者可能会忘记使用b.N，导致基准测试无法正确执行。</li>
<li>误用b.N：开发者可能会错误地将b.N用于其他目的，例如调整算法输入的大小，而不是作为迭代次数。</li>
<li>复杂的计时器管理：基准测试框架无法知道b.N循环何时开始，因此如果基准测试有复杂的设置（setup），开发者需要手动调用ResetTimer来重置计时器，这提高了开发人员使用benchmark函数的门槛，还非常容易出错。</li>
</ul>
<p>为了解决上述问题，Austin Clements提议在testing.B中添加一个新的方法Loop，并鼓励开发者使用Loop而不是b.N：</p>
<pre><code>func (b *B) Loop() bool

func Benchmark(b *testing.B) {
    ...(setup)
    for b.Loop() {
        // … benchmark body …
    }
    ...(cleanup)
}
</code></pre>
<p>显然新Loop方法以及基于新Loopfang方法的“新Benchmark”函数有如下优点：</p>
<ul>
<li>避免误用b.N：Loop方法明确地用于基准测试的迭代，开发者无法将其用于其他目的。</li>
<li>自动计时器管理：基准测试框架可以仅记录发生在基准测试操作期间(即for循环内部)的时间和其他指标，因此开发者不再需要手动调用ResetTimer或担心setup的复杂性了。</li>
<li>减少重复设置：Loop方法可以在内部处理迭代启动（ramp-up），这意味着基准测试之前的setup只会执行一次，而不是在每次启动步骤中重复执行。这对于具有复杂设置的基准测试来说，可以节省大量时间。</li>
<li>防止编译器优化：对go编译器来说，Loop方法本身就是一个的明显信号，可阻止某些优化（如内联），以确保基准测试结果的有效性。</li>
<li>支持更丰富的统计分析：将来，Loop方法可以收集值分布而不是仅仅平均值，从而提供更深入的基准测试结果分析。</li>
</ul>
<p>这里也<strong>强烈建议大家在Go 1.24及以后版本中，使用基于B.Loop的新基准测试函数</strong>。</p>
<h3>2.8 <a href="https://github.com/golang/go/issues/69687">增加实验包testing/synctest(#69687)</a></h3>
<p>在Go语言中，测试并发代码一直是一个具有挑战性的任务。传统的测试方法通常依赖于真实的系统时钟和同步机制，这会导致测试变得缓慢且容易出现不确定性（即“flaky”测试）。例如，测试一个带有超时机制的并发缓存时，测试代码可能需要等待几秒钟来验证缓存条目是否在预期时间内过期。这种等待不仅增加了测试的执行时间，还可能导致测试在某些情况下失败，尤其是在CI系统负载较高或执行环境不稳定的情况下。</p>
<p>为了解决这些问题，Go社区提出了一个<a href="https://github.com/golang/go/issues/67434">新的testing/synctest包</a>，旨在简化并发代码的测试。该包的核心思想是通过使用虚拟时钟和goroutine组(也称为气泡(bubble)来控制并发代码的执行，从而使测试既快速又可靠。下面是synctest包的API：</p>
<pre><code>func Run(f func()) {
    synctest.Run(f)
}

func Wait() {
    synctest.Wait()
}
</code></pre>
<p>我们看到synctest包对外仅暴露两个公开函数。</p>
<p>Run函数在一个新的goroutine中执行f函数，并创建一个独立的goroutine组（气泡），确保所有相关的goroutine都在虚拟时钟的控制下执行。气泡内的goroutine不能与气泡外的goroutine直接交互，否则会引发panic。如果所有goroutine都被阻塞且没有定时器被调度，Run会引发panic。Run 会在气泡中的所有goroutine退出后返回。</p>
<p>Wait函数调用后将阻塞，直到当前气泡中的所有其他goroutine都处于持久阻塞状态。该函数用于确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。即确保在测试继续之前所有后台goroutine都已空闲或退出。如果从非气泡的goroutine调用Wait，或者同一气泡中的两个goroutine同时调用Wait，会引发panic。阻塞在系统调用或外部事件（如网络操作）的goroutine不是持久阻塞的，Wait不会等待这些goroutine。</p>
<p>这里再明确一下上面API说明中提到的各种概念：</p>
<ul>
<li>goroutine组（气泡）</li>
</ul>
<p>Run函数创建的goroutine及其间接启动的所有goroutine形成一个独立的“气泡”。气泡内的goroutine使用虚拟时钟，并且气泡内的所有操作（如通道、定时器等）都与该气泡关联。气泡内的goroutine不能与气泡外的goroutine直接交互。</p>
<ul>
<li>虚拟时钟</li>
</ul>
<p>虚拟时钟的初始时间为2000-01-01 00:00:00 UTC。每个气泡有一个虚拟时钟，它只有在所有goroutine都处于阻塞状态时才会推进。这意味着测试代码可以精确控制时间的流逝，而不会受到真实系统时钟的限制。</p>
<ul>
<li>持久阻塞</li>
</ul>
<p>一个goroutine如果只能被气泡内的另一个goroutine解除阻塞，则称其为持久阻塞。以下操作会使goroutine持久阻塞：</p>
<pre><code>- 在气泡内向通道发送或接收数据
- 在select语句中，每个case都是气泡内的通道
- sync.Cond.Wait
- time.Sleep
</code></pre>
<p>下面是一个使用testing/synctest进行测试的简单示例，我们有一个Cache结构：</p>
<pre><code>// go1.24-foresight/stdlib/synctest/cache.go

package main

import (
    "sync"
    "time"
)

// Cache 是一个泛型并发缓存，支持任意类型的键和值。
type Cache[K comparable, V any] struct {
    mu      sync.Mutex
    items   map[K]cacheItem[V]
    expiry  time.Duration
    creator func(K) V
}

// cacheItem 是缓存中的单个条目，包含值和过期时间。
type cacheItem[V any] struct {
    value     V
    expiresAt time.Time
}

// NewCache 创建一个新的缓存，带有指定的过期时间和创建新条目的函数。
func NewCache[K comparable, V any](expiry time.Duration, f func(K) V) *Cache[K, V] {
    return &amp;Cache[K, V]{
        items:   make(map[K]cacheItem[V]),
        expiry:  expiry,
        creator: f,
    }
}

// Get 返回缓存中指定键的值，如果键不存在或已过期，则创建新条目。
func (c *Cache[K, V]) Get(key K) V {
    c.mu.Lock()
    defer c.mu.Unlock()

    // 检查缓存中是否存在该键
    item, exists := c.items[key]

    // 如果键存在且未过期，返回缓存的值
    if exists &amp;&amp; time.Now().Before(item.expiresAt) {
        return item.value
    }

    // 如果键不存在或已过期，创建新条目
    value := c.creator(key)
    c.items[key] = cacheItem[V]{
        value:     value,
        expiresAt: time.Now().Add(c.expiry),
    }

    return value
}
</code></pre>
<p>上述代码实现了一个简单的并发缓存，支持泛型键和值，并且具有过期机制。通过使用sync.Mutex来保护对缓存条目的并发访问，确保了线程安全。Get方法在键不存在或已过期时，会调用creator函数创建新条目，并更新缓存。</p>
<p>下面是对上面Cache结构进行并发测试的代码：</p>
<pre><code>// go1.24-foresight/stdlib/synctest/cache_test.go

package main

import (
    "testing"
    "testing/synctest"
    "time"
)

func TestCacheEntryExpires(t *testing.T) {
    synctest.Run(func() {
        count := 0
        c := NewCache(2*time.Second, func(key string) int {
            count++
            return count
        })

        // Get an entry from the cache.
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Verify that we get the same entry when accessing it before the expiry.
        time.Sleep(1 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Wait for the entry to expire and verify that we now get a new one.
        time.Sleep(3 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 2; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }
    })
}
</code></pre>
<p>通过使用synctest.Run和synctest.Wait，上述测试代码能够在虚拟时钟的控制下<strong>验证Cache的过期机制</strong>。synctest.Run创建了一个独立的goroutine组，确保所有相关的goroutine都在虚拟时钟的控制下执行。synctest.Wait确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。</p>
<p>使用gotip执行该测试：</p>
<pre><code>$GOEXPERIMENT=synctest  gotip test -v
=== RUN   TestCacheEntryExpires
--- PASS: TestCacheEntryExpires (0.00s)
PASS
ok      demo    0.002s
</code></pre>
<p>我们可以瞬间得到结果，而<strong>无需等待代码中的Sleep秒数</strong>。</p>
<h3>2.9 其他一些变化</h3>
<ul>
<li><a href="https://github.com/golang/go/issues/62005">log/slog: 增加slog.DiscardHandler(#62005)</a></li>
</ul>
<p>slog包添加包级变量slog.DiscardHandler （类型为slog.Handler ），它将丢弃所有日志输出。</p>
<ul>
<li><a href="https://github.com/golang/go/issues/61901">bytes和strings增加一些iterator(#61901)</a></li>
</ul>
<p>下面是五个返回迭代器的新增函数，以strings包为例：</p>
<pre><code>- func Lines(s string) iter.Seq[string] 

返回一个迭代器，遍历字符串s中以换行符结尾的行。

- func SplitSeq(s, sep string) iter.Seq[string] 

返回一个迭代器，遍历s中由sep分隔的所有子字符串。  

- func SplitAfterSeq(s, sep string) iter.Seq[string] 

返回一个迭代器，遍历s中在每个sep实例之后分割的子字符串。 

- func FieldsSeq(s string) iter.Seq[string] 

返回一个迭代器，遍历s中由空白字符（由unicode.IsSpace定义）分隔的子字符串。

- func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string] 

返回一个迭代器，遍历s中由满足f(c)的Unicode码点分隔的子字符串。
</code></pre>
<ul>
<li><a href="https://github.com/golang/go/issues/70683">sync.Map的底层实现换成了HashTrieMap(#70683)</a></li>
</ul>
<p>和weak包一样，HashTrieMap同样是实现unique包的副产品，但它的性能很好，在很多情况下都要比sync.Map快很多。于是Michael Knyszek使用HashTrieMap替换了sync.Map的底层实现。</p>
<p>当然，如果你不满意HashTrieMap的表现，你也可以使用GOEXPERIMENT=nosynchashtriemap恢复到sync.Map之前的实现。</p>
<ul>
<li><a href="https://github.com/golang/go/issues/67816">net/http: 支持非加密的http/2(#67816)</a></li>
</ul>
<p>在Go语言的net/http包中，HTTP/2的支持默认是通过TLS加密的连接来实现的，通常称为”h2&#8243;。然而，HTTP/2也可以在不加密的TCP连接上运行，这种模式被称为”h2c”（HTTP/2 Clear Text）。尽管golang.org/x/net/http2/h2c包提供了对h2c的支持，但这种支持并不直接集成到net/http包中，导致用户在使用h2c时需要进行复杂的配置和处理。因此，社区提出了将h2c支持直接集成到net/http包中的issue，以简化用户的使用体验。</p>
<p>直接集成h2c支持后，将使得Go语言的HTTP/2功能更加完整，用户可以更方便地在未加密的连接上使用HTTP/2。</p>
<h2>3. 其它</h2>
<h3>3.1 <a href="https://github.com/golang/go/issues/65199">支持go:wasmexport指示符(#65199)</a></h3>
<p>Go语言在WebAssembly（Wasm）的支持方面已经有了一定的进展，特别是在<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21版本</a>引入了<a href="https://github.com/golang/go/issues/59149">go:wasmimport指示符</a>，使得<a href="https://go.dev/blog/wasi">Go代码可以调用Wasm宿主定义的函数</a>。然而，目前仍然无法从Wasm宿主调用Go代码。这对于一些需要扩展功能的应用来说是一个限制，例如Envoy、Istio、VS Code等应用，它们允许通过调用Wasm编译的代码来扩展功能。但Go目前无法支持这些应用，因为Go编译的Wasm模块中唯一导出的函数是&#95;start，对应于main包中的main函数。</p>
<p>但Go社区对导出Go函数为wasm有着迫切的需求，同时，导出函数到Wasm宿主也是实现GOOS=wasip2的必要条件(<a href="https://github.com/WebAssembly/WASI/blob/main/preview2/README.md">wasip2是WASI规范的预览2版本</a>)。</p>
<p>于是issue 65199给出了导出Go函数到Wasm的落地方案。该issue提议在库模式下(即导出的Go函数供其他基于wasm运行时库开发的应用使用)，重用-buildmode构建标志值c-shared，用于wasip1。它现在向编译器发出信号，要求用&#95;initialize函数替换&#95;start函数，该函数执行运行时和包的初始化：</p>
<pre><code>$gotip help buildmode
... ...
    -buildmode=c-shared
        Build the listed main package, plus all packages it imports,
        into a C shared library. The only callable symbols will
        be those functions exported using a cgo //export comment.
        On wasip1, this mode builds it to a WASI reactor/library,
        of which the callable symbols are those functions exported
        using a //go:wasmexport directive. Requires exactly one
        main package to be listed.
... ...
</code></pre>
<p>新增一个编译器指示符go:wasmexport，用于向编译器发出信号，表明某个函数应该使用Wasm导出（Wasm export），在生成的Wasm二进制文件中导出。该指示符只能在GOOS=wasip1时使用，否则会导致编译失败。</p>
<pre><code>//go:wasmexport name
</code></pre>
<p>其中name是导出函数的名称，该参数是必需的。<strong>该指示符只能用于函数，不能用于方法</strong>。</p>
<p>该issue由Johan Brandhorst提出，但最终是由CherryMui给出了最终实现，并且CherryMui还给出了一个<a href="https://go.googlesource.com/scratch/+/refs/heads/master/cherry/wasmtest/">应用go:wasmexport的example</a>，这个example演示了go:wasmexport在库模式下的应用方法。例子代码较多，这里我做了一个裁剪，下面是裁剪后的代码和使用方法，大家可以参考一下。</p>
<p>示例的结构如下：</p>
<pre><code>$tree -F ./wasmtest
./wasmtest
├── Makefile
├── go.mod
├── go.sum
├── testprog/
│   └── x.go
└── w.go
</code></pre>
<p>其中testprog/x.go中导出了一个Add函数：</p>
<pre><code>// go1.24-foresight/wasmtest/testprog/x.go

package main

func init() {
    println("init function called")
}

//go:wasmexport Add
func Add(a, b int64) int64 {
    return a+b
}

func main() {
        println("hello")
}
</code></pre>
<p>我们将x.go编译为x.wasm文件：</p>
<pre><code>$GOARCH=wasm GOOS=wasip1 gotip build -buildmode=c-shared -o x.wasm ./testprog
</code></pre>
<p>然后在w.go中使用x.wasm中的Add函数：</p>
<pre><code>// go1.24-foresight/wasmtest/w.go

package main
import (
    "context"
    "fmt"
    "os"
    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/api"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)
    buf, err := os.ReadFile(os.Args[1])
    if err != nil {
        panic(err)
    }
    config := wazero.NewModuleConfig().
        WithStdout(os.Stdout).WithStderr(os.Stderr).
        WithStartFunctions() // don't call _start
    wasi_snapshot_preview1.MustInstantiate(ctx, r)
    m, err := r.InstantiateWithConfig(ctx, buf, config)
    if err != nil {
        panic(err)
    }

    // get export functions from the module
    F := func(a int64, b int64) int64 {
        exp := m.ExportedFunction("Add")
        r, err := exp.Call(ctx, api.EncodeI64(a), api.EncodeI64(b))
        if err != nil {
            panic(err)
        }
            rr := int64(r[0])
                fmt.Printf("host: Add %d + %d = %d\n", a,b,rr)
                return rr
    }

    // Library mode.
    entry := m.ExportedFunction("_initialize")
    fmt.Println("Library mode: initialize")
    _, err = entry.Call(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println("\nLibrary mode: call export functions")
    println(F(5,6))
}
</code></pre>
<p>运行上述w.go，我们将得到以下预期结果：</p>
<pre><code>$gotip run w.go ./x.wasm
Library mode: initialize
init function called

Library mode: call export functions
host: Add 5 + 6 = 11
11
</code></pre>
<h3>3.2 移植(porting)</h3>
<ul>
<li>Linux：要求内核版本不低于3.2。</li>
<li>macOS：Go 1.24是支持macOS 11 Big Sur的最后一个版本。</li>
<li>Windows：提升对Nano Server和内置服务帐户的支持，并修复域环境中的性能问题。</li>
<li>支持的Unicode版本升级到15.1.0。</li>
</ul>
<h2>4. 小结</h2>
<p>本文详细介绍了即将发布的Go 1.24版本在工具链和标准库方面的重要新特性。这些新特性不仅简化了工具的使用，提升了开发体验，还增强了标准库的功能和安全性，特别是在加密、并发测试等方面。通过这些改进，Go语言将继续朝着更高效、更安全、更易用的方向发展。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.24-foresight">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/milestone/322">Go 1.24 milestone</a> &#8211; https://github.com/golang/go/milestone/322</li>
<li><a href="https://tip.golang.org/doc/go1.24">Go 1.24 Release Notes Draft</a> &#8211; https://tip.golang.org/doc/go1.24</li>
<li><a href="https://dev.golang.org/release">Go Release Dashboard</a> &#8211; https://dev.golang.org/release</li>
<li><a href="https://tip.golang.org/ref/spec">Go spec tip</a> &#8211; https://tip.golang.org/ref/spec</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/12/17/go-1-24-foresight-part2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go开发者的密码学导航：crypto库使用指南</title>
		<link>https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive/</link>
		<comments>https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive/#comments</comments>
		<pubDate>Fri, 18 Oct 2024 23:22:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ACME]]></category>
		<category><![CDATA[AdamLangley]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Argon2]]></category>
		<category><![CDATA[Bcrypt]]></category>
		<category><![CDATA[black2s]]></category>
		<category><![CDATA[blake2b]]></category>
		<category><![CDATA[Block]]></category>
		<category><![CDATA[blowfish]]></category>
		<category><![CDATA[bn256]]></category>
		<category><![CDATA[boriingssl]]></category>
		<category><![CDATA[cast5]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[chacha20poly1305]]></category>
		<category><![CDATA[cipher]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[DES]]></category>
		<category><![CDATA[dsa]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[ECDSA]]></category>
		<category><![CDATA[Ed25519]]></category>
		<category><![CDATA[elliptic]]></category>
		<category><![CDATA[FilippoValsorda]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[hkdf]]></category>
		<category><![CDATA[HMAC]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[IO]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[md4]]></category>
		<category><![CDATA[MD5]]></category>
		<category><![CDATA[nacl]]></category>
		<category><![CDATA[ocsp]]></category>
		<category><![CDATA[openpgp]]></category>
		<category><![CDATA[otr]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PBKDF2]]></category>
		<category><![CDATA[pkcs12]]></category>
		<category><![CDATA[Poly1305]]></category>
		<category><![CDATA[quantum]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[rc44]]></category>
		<category><![CDATA[reader]]></category>
		<category><![CDATA[ripemd160]]></category>
		<category><![CDATA[RolandShoemaker]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[salsa20]]></category>
		<category><![CDATA[Scrypt]]></category>
		<category><![CDATA[SHA1]]></category>
		<category><![CDATA[SHA256]]></category>
		<category><![CDATA[sha3]]></category>
		<category><![CDATA[sign]]></category>
		<category><![CDATA[signer]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[std]]></category>
		<category><![CDATA[stream]]></category>
		<category><![CDATA[tea]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[twofish]]></category>
		<category><![CDATA[writer]]></category>
		<category><![CDATA[x509]]></category>
		<category><![CDATA[xtea]]></category>
		<category><![CDATA[xts]]></category>
		<category><![CDATA[依赖]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[后量子密码学]]></category>
		<category><![CDATA[向后兼容]]></category>
		<category><![CDATA[哈希]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[对称加密]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[架构]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[椭圆曲线]]></category>
		<category><![CDATA[消息认证码]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[签名]]></category>
		<category><![CDATA[随机数]]></category>
		<category><![CDATA[非对称加密]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4348</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive Go号称“开箱即用”，这与其标准库的丰富功能和高质量是分不开的。而在Go标准库中，crypto库(包括crypto包、crypto目录下相关包以及golang.org/x/crypto下的补充包)又是Go社区最值得称道的Go库之一。 crypto库由Go核心团队维护，确保了最高级别的安全标准和及时的漏洞修复，为开发者提供了可靠的安全保障。crypto还涵盖了从基础的对称加密到复杂的非对称加密，以及各种哈希函数和数字签名算法等广泛的加解密算法支持，以满足Go开发者的各种需求为目的，而不是与其他密码学工具包竞争。此外，crypto库还经过精心优化，能够在不同硬件平台上尽可能地保证高效的执行性能。值得一提的是，crypto库还提供了统一的API设计，使得不同加密算法的使用方式保持一致，也降低了开发者的学习成本。 可以说Go crypto库是Go生态中密码学功能的核心，它为Go开发者提供了一套全面、安全、保持现代化、提供安全默认值且易于使用的密码学工具，使得在Go应用程序中实现各种密码学功能需求时变得简单而可靠。 不过要理解并得心应手的使用crypto库中的相关密码学包仍然并非易事，这是因为密码学涉及数学、密码分析、计算机安全等多个学科，概念多，算法也十分复杂，而大多程序员对密码学的了解又多停留在使用层面，缺乏对其原理和底层机制的深入认知，甚至连每个包的用途都不甚了解。这导致很多开发者浏览了crypto相关包之后，甚至不知道该使用哪个包。 所以在这篇文章中，我想为Go开发者建立一张crypto库的“地图”，这张“地图”将帮助我们从宏观角度理解crypto库的结构，帮助大家快速精准选择正确的包。并且通过对crypto相关包设计的理解，轻松掌握crypto相关包的使用模式。 注：Go标准库crypto库的第一任负责人是Adam Langley(agl)，他开创了Go crypto库，他在招募和培养了Filippo Valsorda后离开了Go项目，后者成为了Go crypto的负责人。Filippo在Go项目工作若干年后，把负责人交给了Roland Shoemaker，即现任Go团队安全组的负责人。当然Shoemaker也是Filippo招募到Go团队中的。 下面我们首先来看看Go crypto库的“整体架构”。 1. 标准库crypto与golang.org/x/crypto Go的密码学功能(即我们统一称的crypto库)分为两个主要部分：标准库的crypto相关包和扩展库golang.org/x/crypto。这种分离设计有其特定的目的和优势： Go标准库的crypto相关包，包含了最基础、最稳定和使用最广泛的密码学算法。这些算法实现经过Go团队的严格审查，保证了长期稳定性和向后兼容性。同时，这些包是随Go安装包分发的，使用时再无需引入额外的依赖。 而golang.org/x/crypto则号称是Go标准库crypto相关包的补充库，虽然它同样由Go团队维护，但由于不是标准库，它可以包含更多实验性或较新的密码学算法及实现，并可以更快速的迭代和更新。这样它也可以成为Go标准库中一些crypto相关包的“孵化器”，就像当年golang.org/x/net/context提升为标准库context一样。 同时golang.org/x/crypto也是Go标准库依赖的为数极少的外部包之一。比如，下面是Go 1.23.0标准库go.mod文件的内容： module std go 1.23 require ( golang.org/x/crypto v0.23.1-0.20240603234054-0b431c7de36a golang.org/x/net v0.25.1-0.20240603202750-6249541f2a6c ) require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect ) 我们看到Go标准库依赖特定版本的golang.org/x/crypto模块。 与标准库不同的是，如果你要使用golang.org/x/crypto模块中的密码学包，你就需要单独引入项目依赖。此外，golang.org/x 下的包通常被视为实验性或扩展包，因此它们并不严格遵循Go1兼容性承诺。换句话说，这些包在API稳定性上没有与标准库相同的保证，可能会有非向后兼容的更改。 综上，我们看到Go标准库crypto与golang.org/x/crypto的这种分离策略，允许Go团队在保持标准库稳定性的同时，也能够灵活地引入新的密码学算法和技术。 接下来，我们来看看crypto库的整体结构设计原则，这些原则对理解整个crypto库大有裨益。 2. 整体结构设计原则 Go的crypto库整体上的结构设计遵循了几个原则： 2.1 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-crypto-package-design-deep-dive-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive">本文永久链接</a> &#8211; https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive</p>
<p>Go号称“开箱即用”，这与其标准库的丰富功能和高质量是分不开的。而在Go标准库中，crypto库(包括crypto包、crypto目录下相关包以及golang.org/x/crypto下的补充包)又是Go社区最值得称道的Go库之一。</p>
<p>crypto库由Go核心团队维护，确保了最高级别的安全标准和及时的漏洞修复，为开发者提供了可靠的安全保障。crypto还涵盖了从基础的对称加密到复杂的非对称加密，以及各种哈希函数和数字签名算法等广泛的加解密算法支持，以满足Go开发者的各种需求为目的，而不是与其他密码学工具包竞争。此外，crypto库还经过精心优化，能够在不同硬件平台上尽可能地保证高效的执行性能。值得一提的是，crypto库还提供了统一的API设计，使得不同加密算法的使用方式保持一致，也降低了开发者的学习成本。</p>
<p>可以说<a href="https://pkg.go.dev/crypto">Go crypto库</a>是<strong>Go生态中密码学功能的核心</strong>，它为Go开发者提供了一套全面、安全、<strong>保持现代化</strong>、<strong>提供安全默认值</strong>且<strong>易于使用</strong>的密码学工具，使得在Go应用程序中实现各种密码学功能需求时变得简单而可靠。</p>
<p>不过要理解并得心应手的使用crypto库中的相关密码学包仍然并非易事，这是因为密码学涉及数学、密码分析、计算机安全等多个学科，概念多，算法也十分复杂，而大多程序员对密码学的了解又多停留在使用层面，缺乏对其原理和底层机制的深入认知，甚至连每个包的用途都不甚了解。这导致很多开发者浏览了crypto相关包之后，甚至不知道该使用哪个包。</p>
<p>所以在这篇文章中，我想为Go开发者建立一张crypto库的“地图”，这张“地图”将帮助我们从宏观角度理解crypto库的结构，帮助大家快速精准选择正确的包。并且通过对crypto相关包设计的理解，轻松掌握crypto相关包的使用模式。</p>
<blockquote>
<p>注：Go标准库crypto库的第一任负责人是<a href="https://www.imperialviolet.org/">Adam Langley(agl)</a>，他开创了Go crypto库，他在招募和培养了<a href="https://blog.filippo.io/">Filippo Valsorda</a>后离开了Go项目，后者成为了Go crypto的负责人。Filippo在Go项目工作若干年后，把负责人交给了<a href="https://github.com/rolandshoemaker">Roland Shoemaker</a>，即现任Go团队安全组的负责人。当然Shoemaker也是Filippo招募到Go团队中的。</p>
</blockquote>
<p>下面我们首先来看看Go crypto库的“整体架构”。</p>
<h2>1. 标准库crypto与golang.org/x/crypto</h2>
<p>Go的密码学功能(即我们统一称的crypto库)分为两个主要部分：<strong>标准库的crypto相关包和扩展库golang.org/x/crypto</strong>。这种分离设计有其特定的目的和优势：</p>
<p>Go标准库的crypto相关包，包含了最基础、最稳定和使用最广泛的密码学算法。这些算法实现经过Go团队的严格审查，保证了长期稳定性和向后兼容性。同时，这些包是随Go安装包分发的，使用时再无需引入额外的依赖。</p>
<p>而golang.org/x/crypto则号称是Go标准库crypto相关包的补充库，虽然它同样由Go团队维护，但由于不是标准库，它可以包含更多实验性或较新的密码学算法及实现，并可以更快速的迭代和更新。这样它也可以成为Go标准库中一些crypto相关包的“孵化器”，就像当年golang.org/x/net/context提升为<a href="https://tonybai.com/2022/11/08/understand-go-context-by-example">标准库context</a>一样。</p>
<p>同时golang.org/x/crypto也是Go标准库依赖的为数极少的外部包之一。比如，下面是<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23/">Go 1.23.0</a>标准库go.mod文件的内容：</p>
<pre><code>module std

go 1.23

require (
    golang.org/x/crypto v0.23.1-0.20240603234054-0b431c7de36a
    golang.org/x/net v0.25.1-0.20240603202750-6249541f2a6c
)

require (
    golang.org/x/sys v0.22.0 // indirect
    golang.org/x/text v0.16.0 // indirect
)
</code></pre>
<p>我们看到Go标准库依赖特定版本的golang.org/x/crypto模块。</p>
<p>与标准库不同的是，如果你要使用golang.org/x/crypto模块中的密码学包，<strong>你就需要单独引入项目依赖</strong>。此外，golang.org/x 下的包通常被视为实验性或扩展包，因此它们并不严格遵循<a href="https://go.dev/doc/go1compat">Go1兼容性承诺</a>。换句话说，这些包在API稳定性上没有与标准库相同的保证，可能会有非向后兼容的更改。</p>
<p>综上，我们看到Go标准库crypto与golang.org/x/crypto的这种分离策略，允许Go团队在保持标准库稳定性的同时，也能够灵活地引入新的密码学算法和技术。</p>
<p>接下来，我们来看看crypto库的整体结构设计原则，这些原则对理解整个crypto库大有裨益。</p>
<h2>2. 整体结构设计原则</h2>
<p>Go的crypto库整体上的结构设计遵循了几个原则：</p>
<h3>2.1 统一接口和类型抽象</h3>
<p>首先是<strong>统一接口和类型抽象</strong>，这在最顶层的crypto包中就能充分体现。</p>
<p>crypto包定义了一个Hash类型和一个创建具体哈希实现的方法。这个设计允许统一管理不同的哈希算法，同时保持了良好的可扩展性：</p>
<pre><code>// $GOROOT/src/crypto/crypto.go

type Hash uint

// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
func (h Hash) New() hash.Hash {
    if h &gt; 0 &amp;&amp; h &lt; maxHash {
        f := hashes[h]
        if f != nil {
            return f()
        }
    }
    panic("crypto: requested hash function #" + strconv.Itoa(int(h)) + " is unavailable")
}

// HashFunc simply returns the value of h so that [Hash] implements [SignerOpts].
func (h Hash) HashFunc() Hash {
    return h
}

// RegisterHash registers a function that returns a new instance of the given
// hash function. This is intended to be called from the init function in
// packages that implement hash functions.
func RegisterHash(h Hash, f func() hash.Hash) {
    if h &gt;= maxHash {
        panic("crypto: RegisterHash of unknown hash function")
    }
    hashes[h] = f
}

var hashes = make([]func() hash.Hash, maxHash)
</code></pre>
<p>Hash类型作为一个统一的标识符，用于表示不同的哈希算法。New方法则“像一个工厂方法”，用于创建具体的哈希实现。新的哈希算法可以很容易地添加到这个系统中，只需定义一个新的常量并提供相应的实现，并将实现通过RegisterHash注册到hashes中即可。下面是一个使用sha256算法的示例(仅做演示，并非惯例写法)：</p>
<pre><code>package main

import (
    "crypto"
    _ "crypto/sha256" // register h256 to hashes
)

func main() {
    ht := crypto.SHA256
    h := ht.New()
    h.Write([]byte("hello world"))
    sum := h.Sum(nil)
    println(sum)
}
</code></pre>
<blockquote>
<p>注：也许是早期标准库的设计问题，hash接口目前没有放到crypto下面，而是在标准库顶层目录下。crypto库中的hash实现通过New方法返回真正的hash.Hash实现。</p>
</blockquote>
<p>crypto包还定义了几个关键接口，这些接口被各个子包实现，从而实现了高度的可扩展性和互操作性，比如下面的Signer、SignerOpts、Decrypter接口：</p>
<pre><code>// Signer is an interface for an opaque private key that can be used for
// signing operations. For example, an RSA key kept in a hardware module.
type Signer interface {
    Public() PublicKey
    Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}

// SignerOpts contains options for signing with a [Signer].
type SignerOpts interface {
    HashFunc() Hash
}

// Decrypter is an interface for an opaque private key that can be used for
// asymmetric decryption operations. An example would be an RSA key
// kept in a hardware module.
type Decrypter interface {
    Public() PublicKey
    Decrypt(rand io.Reader, msg []byte, opts DecrypterOpts) (plaintext []byte, err error)
}
</code></pre>
<p>以Signer接口为例，这个Signer接口为不同的签名算法（如RSA、ECDSA、Ed25519等）提供了一个统一的抽象。下面是一个使用统一Signer接口但不同Signer实现的示例：</p>
<pre><code>func signData(signer crypto.Signer, data []byte) ([]byte, error) {
    hash := crypto.SHA256
    h := hash.New()
    h.Write(data)
    digest := h.Sum(nil)

    return signer.Sign(rand.Reader, digest, hash)
}

func main() {
    rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)
    signature, _ := signData(rsaKey, []byte("Hello, World!"))
    println(signature)

    ecdsaKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    signature, _ = signData(ecdsaKey, []byte("Hello, World!"))
    println(signature)
}
</code></pre>
<p>在这个例子中，我们看到了如何使用相同的signData函数来处理不同类型的签名算法，这体现了统一接口带来的灵活性和一致性。</p>
<p>在crypto目录下的各个子包中，上述原则也有很好的体现，比如cipher包就定义了Block、Stream等接口，然后aes、des等对称加密包也都提供了创建实现了这些接口的类型的函数，比如aes.NewCipher以及des.NewCipher等。</p>
<h3>2.2 模块化</h3>
<p>每个子包专注于特定的功能，这种模块化设计使得每个包都相对独立，便于维护和使用。以aes包和des包为例：</p>
<pre><code>// crypto/aes/cipher.go
func NewCipher(key []byte) (cipher.Block, error) {
    // AES specific implementation
}

// crypto/des/cipher.go
func NewCipher(key []byte) (cipher.Block, error) {
    // DES specific implementation
}
</code></pre>
<p>这两个包都实现了相同的NewCipher函数，但内部实现完全不同，专注于各自的加密算法。</p>
<h3>2.3 易用性与灵活性的平衡</h3>
<p>Go crypto库中的很多包既提供了可以满足大多数常见用例的需求、易用性很好的高级API，同时也提供了更灵活的低级API，允许开发者在需要时进行更精细的控制或自定义实现。</p>
<p>让我们以SHA256哈希函数为例来说明这一点：</p>
<pre><code>// 高级API
func highLevelAPI(data []byte) [32]byte {
    return sha256.Sum256(data)
}

// 低级API
func lowLevelAPI(data []byte) [32]byte {
    h := sha256.New()
    h.Write(data)
    return *(*[32]byte)(h.Sum(nil))
}

func main() {
    fmt.Println(lowLevelAPI([]byte("hello world")))
    fmt.Println(highLevelAPI([]byte("hello world")))
}
</code></pre>
<p>在这个例子中，sha256.Sum256是高级API，而lowLevelAPI中使用的那套逻辑则是对低级API的组合以实现Sum256功能。</p>
<h3>2.4 可扩展性</h3>
<p>基于“统一接口和类型抽象”原则设计的crypto库可以让用户轻松地集成自己的实现或第三方库，这种可扩展性便于我们添加新的算法或功能，而不影响现有结构。 比如，我们可以像这下面这样实现自定义的cipher.Block：</p>
<pre><code>type MyCustomCipher struct {
    // ...
}

func (c *MyCustomCipher) BlockSize() int {
    // ...
}

func (c *MyCustomCipher) Encrypt(dst, src []byte) {
    // ...
}

func (c *MyCustomCipher) Decrypt(dst, src []byte) {
    // ...
}
</code></pre>
<p>之后，这个自定义的cipher.Block实现便可以直接用在标准库提供的分组密码模式中。</p>
<p>作为crypto库的扩展和实验库，golang.org/x/crypto也遵循了与标准库crypto相关包一致的设计原则，这里就不举例说明了。</p>
<p>有了上述对crypto库的整体设计原则的认知后，我们再来看一下Go标准库crypto目录下的子包结构，了解了这个结果，<strong>你就会像拥有了crypto库的“导航”</strong>，可以顺利方便地找到你想要的密码学包了。</p>
<h2>3. 子包结构概览</h2>
<p>众所周知，Go标准库crypto目录下不仅有crypto包，还有众多种类的密码学包，下面这张示意图对这些包进行了简单分类：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-crypto-package-design-deep-dive-2.png" alt="" /></p>
<p>下面我会按照图中的类别对各个包做简单介绍，包括功能、用途、简单的示例以及是否推荐使用。密码学一直在发展，很多算法因为不再“牢不可破”而逐渐不再被推荐使用。但Go为了保证Go1兼容性，这些包依赖留在了Go标准库中。</p>
<p>我们自上而下，先从哈希函数开始。</p>
<h3>3.1 哈希函数</h3>
<h4>3.1.1 md5</h4>
<ul>
<li>功能：实现MD5哈希算法</li>
<li>用途：生成数据的128位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/md5"
hash := md5.Sum([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：不推荐用于安全相关用途，因为MD5已被证明不够安全。</li>
</ul>
<h4>3.1.2 sha1</h4>
<ul>
<li>功能：实现SHA-1哈希算法</li>
<li>用途：生成数据的160位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/sha1"
hash := sha1.Sum([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：不推荐用于安全相关用途，因为SHA-1已被证明存在碰撞风险。</li>
</ul>
<h4>3.1.3 sha256</h4>
<ul>
<li>功能：实现SHA-256哈希算法</li>
<li>用途：生成数据的256位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/sha256"
hash := sha256.Sum256([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：推荐使用，安全性高。</li>
</ul>
<h4>3.1.4 sha512</h4>
<ul>
<li>功能：实现SHA-512哈希算法</li>
<li>用途：生成数据的512位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/sha512"
hash := sha512.Sum512([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：推荐使用，安全性很高。</li>
</ul>
<h3>3.2 加密和解密</h3>
<h4>3.2.1 aes</h4>
<ul>
<li>功能：实现AES(Advanced Encryption Standard)对称加密算法</li>
<li>用途：数据对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/aes"
key := []byte("example key 1234") // 16字节的key
block, _ := aes.NewCipher(key)
</code></pre>
<ul>
<li>使用建议：推荐使用，是目前最广泛使用的对称加密算法。</li>
</ul>
<h4>3.2.2 des</h4>
<ul>
<li>功能：实现DES(Data Encryption Standard)和Triple DES加密算法</li>
<li>用途：数据对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/des"
key := []byte("example!") // 8字节的key
block, _ := des.NewCipher(key)
</code></pre>
<ul>
<li>使用建议：不推荐使用DES，密钥长度不足(DES使用56位密钥，实际上是64位，但其中8位是奇偶校验位，不用于加密)，容易被暴力破解。推荐使用AES；Triple DES在某些遗留系统中仍在使用。</li>
</ul>
<h4>3.2.3 rc4</h4>
<ul>
<li>功能：实现RC4(Rivest Cipher 4)流加密算法</li>
<li>用途：流数据的加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/rc4"
key := []byte("secret key")
cipher, _ := rc4.NewCipher(key)
</code></pre>
<ul>
<li>使用建议：不推荐使用，因为RC4已被证明存在安全漏洞。由于这些已知的安全问题，RC4已经被许多现代加密协议和应用所弃用。例如，TLS（Transport Layer Security）协议已经移除了对RC4的支持。</li>
</ul>
<h4>3.2.4 cipher</h4>
<ul>
<li>功能：定义了块加密的通用接口</li>
<li>用途：为其他加密算法提供通用的加密和解密方法</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/cipher"
// 使用AES-GCM模式
block, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(block)
</code></pre>
<ul>
<li>使用建议：推荐使用，特别是GCM等认证加密模式。</li>
</ul>
<h3>3.3 签名和验证</h3>
<h4>3.3.1 dsa</h4>
<ul>
<li>功能：实现数字签名算法（DSA, Digital Signature Algorithm）</li>
<li>用途：生成和验证数字签名</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/dsa"
var privateKey dsa.PrivateKey
dsa.GenerateKey(&amp;privateKey, rand.Reader)
</code></pre>
<ul>
<li>使用建议：目前的趋势是DSA在许多应用中不再被推荐使用。DSA的安全性高度依赖于密钥长度。随着计算能力的提升，较短的DSA密钥长度（例如1024位）已经不再被认为是安全的。NIST建议使用更长的密钥长度（例如2048位或更长），但这会增加计算复杂性和资源消耗。ECDSA使用椭圆曲线密码学，可以在更短的密钥长度下提供相同级别的安全性。</li>
</ul>
<h4>3.3.2 ecdsa</h4>
<ul>
<li>功能：实现椭圆曲线数字签名算法（ECDSA, Elliptic Curve Digital Signature Algorithm）</li>
<li>用途：生成和验证数字签名</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/ecdsa"
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，安全性高且效率好。</li>
</ul>
<h4>3.3.3 ed25519</h4>
<ul>
<li>功能：实现Ed25519签名算法(Edwards-curve Digital Signature Algorithm with Curve25519)</li>
<li>用途：生成和验证数字签名</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/ed25519"
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，安全性高且性能优秀。Ed25519提供了比传统ECDSA更高的安全性和性能，同时减少了某些类型的实现风险。因此，在选择数字签名算法时，Ed25519是一个非常有吸引力的选项，尤其是在需要高性能和强安全保障的应用中。</li>
</ul>
<h4>3.3.4 rsa</h4>
<ul>
<li>功能：实现RSA(Rivest–Shamir–Adleman)加密和签名算法</li>
<li>用途：非对称加密、数字签名</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/rsa"
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
</code></pre>
<ul>
<li>使用建议：关于是否推荐使用RSA，这取决于具体的应用场景和安全需求。RSA在许多应用中仍然被广泛使用，尤其是在需要公钥加密和数字签名的场景。它是一个经过时间考验的算法，有着良好的安全记录。随着计算能力的提升，特别是量子计算的发展，RSA的安全性可能会受到威胁。此外，对于某些高性能或资源受限的环境，RSA可能不如其他算法（如椭圆曲线加密算法，如ECDSA或Ed25519）高效。尤其是签名，ECDSA或Ed25519可能是更好的选择。 </li>
</ul>
<h3>3.4 密钥交换</h3>
<h4>3.4.1 ecdh</h4>
<ul>
<li>功能：实现椭圆曲线Diffie-Hellman密钥交换(Elliptic Curve Diffie-Hellman)</li>
<li>用途：安全地在不安全的通道上协商共享密钥</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/ecdh"
curve := ecdh.P256()
privateKey, _ := curve.GenerateKey(rand.Reader)
</code></pre>
<ul>
<li>使用建议：ECDH是一个强大且高效的密钥交换协议，在许多现代安全通信中被推荐使用，是现代密钥交换的首选方法。</li>
</ul>
<h3>3.5 安全随机数生成</h3>
<h4>3.5.1 rand</h4>
<ul>
<li>功能：提供加密安全的随机数生成器</li>
<li>用途：生成密钥、随机填充等</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/rand"
randomBytes := make([]byte, 32)
rand.Read(randomBytes)
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，不要使用math/rand包(包括math/rand/v2)生成密码学相关的随机数(这些随机数是伪随机)。</li>
</ul>
<h3>3.6 证书和协议</h3>
<h4>3.6.1 tls</h4>
<ul>
<li>功能：实现传输层安全（TLS, Transport Layer Security）协议</li>
<li>用途：安全网络通信</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/tls"
config := &amp;tls.Config{MinVersion: tls.VersionTLS12}
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，是保护网络通信的标准方法。</li>
</ul>
<h4>3.6.2 x509</h4>
<ul>
<li>功能：实现X.509公钥基础设施标准</li>
<li>用途：处理数字证书、证书签名请求（CSR）等</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/x509"
cert, _ := x509.ParseCertificate(certDER)
</code></pre>
<ul>
<li>使用建议：推荐使用，是处理数字证书的标准方法。</li>
</ul>
<h3>3.7. 辅助功能</h3>
<h4>3.7.1 elliptic</h4>
<ul>
<li>功能：实现几个标准的椭圆曲线</li>
<li>用途：为ECDSA和ECDH提供基础</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/elliptic"
curve := elliptic.P256()
</code></pre>
<ul>
<li>使用建议：推荐使用，但通常不直接使用，而是通过ecdsa或ecdh包间接使用。</li>
</ul>
<h4>3.7.2 hmac</h4>
<ul>
<li>功能：实现密钥散列消息认证码（HMAC, Hash-based Message Authentication Code）</li>
<li>用途：消息完整性验证</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/hmac"
h := hmac.New(sha256.New, []byte("secret key"))
h.Write([]byte("message"))
</code></pre>
<ul>
<li>使用建议：推荐使用，是保护数据完整性和消息认证的标准方法。</li>
</ul>
<h4>3.7.3 subtle</h4>
<ul>
<li>功能：提供一些用于实现加密功能的常用但容易出错的操作</li>
<li>用途：比较、常量时间操作等</li>
<li>示例：</li>
</ul>
<pre><code>import "crypto/subtle"
equal := subtle.ConstantTimeCompare([]byte("a"), []byte("b"))
</code></pre>
<ul>
<li>使用建议：推荐在需要时使用，有助于防止时序攻击。</li>
</ul>
<p>结合上面两节，我们看到crypto库的内部依赖结构设计得非常巧妙，以最小化耦合。大多数子包依赖于crypto基础包中定义的接口和类型。crypto/subtle包提供了一些底层的辅助函数，被多个其他包使用。每个加密算法包（如crypto/aes，crypto/rsa）通常是独立的，减少了包间的直接依赖。一些高级功能包（如crypto/tls）会依赖多个基础算法包。大多数需要随机性的包都依赖crypto/rand作为安全随机源。</p>
<p>此外，crypto库与其他Go标准库可紧密集成，包括：</p>
<ul>
<li>与io包集成：使用io.Reader和io.Writer接口，便于流式处理和与其他I/O操作集成。</li>
<li>与encoding相关包集成：比如与encoding/pem和encoding/asn1包配合，用于处理密钥和证书的编码。</li>
<li>与hash包集成：加密哈希函数实现了hash.Hash接口，保持一致性。</li>
<li>与net包集成：如crypto/tls包与net包紧密集成，提供安全的网络通信。</li>
</ul>
<p>接下来，再来看看golang.org/x/crypto扩展库，我们同样借鉴上面的分类和介绍方法，看看crypto扩展库中都有哪些有价值的实用密码学包。</p>
<h2>4 golang.org/x/crypto扩展库</h2>
<p>我们还是从哈希函数开始介绍。</p>
<h3>4.1 哈希函数</h3>
<h4>4.1.1 blake2b和blake2s</h4>
<ul>
<li>功能：实现BLAKE2b和BLAKE2s哈希函数。BLAKE2是一种加密哈希函数，由Jean-Philippe Aumasson、Samuel Neves、Zooko Wilcox-O&#8217;Hearn和Christian Winnerlein设计，旨在替代MD5和SHA-1等旧的哈希函数。BLAKE2有两种主要变体：BLAKE2b和BLAKE2s。</li>
<li>用途：生成高速、安全的哈希值。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/blake2b"
hash := blake2b.Sum256([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：推荐使用，BLAKE2提供了比MD5和SHA-1更高的安全性，同时保持与SHA-2和SHA-3相当的强度，安全性高且速度快。</li>
</ul>
<h4>4.1.2 md4</h4>
<ul>
<li>功能：实现MD4(Message Digest Algorithm 4)哈希算法</li>
<li>用途：生成128位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/md4"
h := md4.New()
h.Write([]byte("hello world"))
hash := h.Sum(nil)
</code></pre>
<ul>
<li>使用建议：不推荐用于安全相关用途，MD4已被证明不安全，容易受到碰撞攻击和其他类型的攻击。已经被更安全的哈希函数所取代，如SHA-2和SHA-3等。</li>
</ul>
<h4>4.1.3 ripemd160</h4>
<ul>
<li>功能：实现RIPEMD-160(RACE Integrity Primitives Evaluation Message Digest 160)哈希算法。</li>
<li>用途：生成160位哈希值</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/ripemd160"
h := ripemd160.New()
h.Write([]byte("hello world"))
hash := h.Sum(nil)
</code></pre>
<ul>
<li>使用建议：RIPEMD-160提供了比MD5和SHA-1更高的安全性，尽管它不像SHA-2和SHA-3那样被广泛研究和使用。但它仍然在某些特定场景（如比特币地址生成）中使用，但一般情况下推荐使用更现代的哈希函数(如SHA-256和SHA-512)。</li>
</ul>
<h4>4.1.4 sha3</h4>
<ul>
<li>功能：实现SHA-3(Secure Hash Algorithm 3)哈希算法族。SHA-3是由美国国家标准与技术研究院（NIST）在2015年发布的一种加密哈希函数，作为SHA-2的后继者。SHA-3的设计基于Keccak算法，由Guido Bertoni、Joan Daemen、Michaël Peeters和Gilles Van Assche开发。</li>
<li>用途：生成不同长度的哈希值。SHA-3包括多种变体，如SHA3-224、SHA3-256、SHA3-384和SHA3-512，分别生成224位、256位、384位和512位的哈希值。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/sha3"
hash := sha3.Sum256([]byte("hello world"))
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，是最新的NIST标准哈希函数。</li>
</ul>
<h3>4.2 加密和解密</h3>
<h4>4.2.1 blowfish</h4>
<ul>
<li>功能：实现Blowfish(设计者Bruce Schneier)加密算法</li>
<li>用途：数据的对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/blowfish"
cipher, _ := blowfish.NewCipher([]byte("key"))
</code></pre>
<ul>
<li>使用建议：不推荐用于新系统，其密钥长度上限为448位，不如更现代的算法安全，建议使用AES。</li>
</ul>
<h4>4.2.2 cast5</h4>
<ul>
<li>功能：实现CAST5（又名CAST-128）加密算法</li>
<li>用途：数据对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/cast5"
cipher, _ := cast5.NewCipher([]byte("16-byte key"))
</code></pre>
<ul>
<li>使用建议：不推荐用于新系统，建议使用AES。</li>
</ul>
<h4>4.2.3 chacha20</h4>
<ul>
<li>功能：实现ChaCha20流加密算法(ChaCha20 stream cipher)</li>
<li>用途：流数据的对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/chacha20"
cipher, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
</code></pre>
<ul>
<li>使用建议：推荐使用，特别是在移动设备上性能优于AES。它被广泛用于各种安全协议和应用中，包括TLS（Transport Layer Security）、SSH（Secure Shell）和QUIC（Quick UDP Internet Connections）等。</li>
</ul>
<h4>4.2.4 salsa20</h4>
<ul>
<li>功能：实现Salsa20流加密算法(Salsa20 stream cipher)</li>
<li>用途：流数据的对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/salsa20"
salsa20.XORKeyStream(dst, src, nonce, key)
</code></pre>
<ul>
<li>使用建议：推荐使用，但ChaCha20可能因其性能优势和更广泛的标准支持而成为更受欢迎的选择。</li>
</ul>
<h4>4.2.4 tea</h4>
<ul>
<li>功能：实现TEA（Tiny Encryption Algorithm）加密算法</li>
<li>用途：轻量级数据加密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/tea"
cipher, _ := tea.NewCipher([]byte("16-byte key"))
</code></pre>
<ul>
<li>使用建议：尽管TEA算法在过去被认为是安全的，但它已经出现了一些已知的安全漏洞，如密钥相关攻击和差分攻击。因此，TEA算法可能不适合需要高安全性的应用。不推荐将它用于新系统，建议使用AES。</li>
</ul>
<h4>4.2.5 twofish</h4>
<ul>
<li>功能：实现Twofish(Twofish block cipher)加密算法</li>
<li>用途：数据对称加密和解密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/twofish"
cipher, _ := twofish.NewCipher([]byte("16, 24, or 32 byte key"))
</code></pre>
<ul>
<li>使用建议：不推荐将它用于新系统，建议使用AES。</li>
</ul>
<h4>4.2.6 xtea</h4>
<ul>
<li>功能：实现XTEA(eXtended Tiny Encryption Algorithm)加密算法</li>
<li>用途：轻量级对称数据加密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/xtea"
cipher, _ := xtea.NewCipher([]byte("16-byte key"))
</code></pre>
<ul>
<li>使用建议：尽管XTEA修复了TEA的一些安全漏洞，但它仍然可能存在其他安全问题，特别是在面对现代计算能力和攻击技术时。因此，不推荐用于新系统，建议使用AES。</li>
</ul>
<h4>4.2.7 xts</h4>
<ul>
<li>功能：实现XTS (XEX-based tweaked-codebook mode with ciphertext stealing) 模式</li>
<li>用途：是一种块加密的标准操作模式，主要用于全磁盘加密</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/xts"
cipher, _ := xts.NewCipher(aes.NewCipher, []byte("32-byte key"))
</code></pre>
<ul>
<li>使用建议：在全磁盘加密场景，即需要对存储设备进行加密的应用中推荐使用。</li>
</ul>
<h3>4.3 认证加密</h3>
<h4>4.3.1 chacha20poly1305</h4>
<ul>
<li>功能：实现ChaCha20-Poly1305(ChaCha20流加密算法和Poly1305消息认证码) AEAD（认证加密与关联数据）。</li>
<li>用途：提供加密和认证的组合</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/chacha20poly1305"
aead, _ := chacha20poly1305.New(key)
</code></pre>
<ul>
<li>使用建议：ChaCha20-Poly1305是一个高效且安全的组合加密算法，在许多现代安全应用中被推荐使用。这里也强烈推荐使用，提供了高安全性和高性能。</li>
</ul>
<h3>4.4 密钥派生和密码哈希</h3>
<h4>4.4.1 argon2</h4>
<ul>
<li>功能：实现Argon2(Argon2 memory-hard key derivation function)密码哈希算法</li>
<li>用途：安全地存储密码</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/argon2"
hash := argon2.IDKey([]byte("password"), salt, 1, 64*1024, 4, 32)
</code></pre>
<ul>
<li>使用建议：强烈推荐使用，是最新的密码哈希标准。</li>
</ul>
<h4>4.4.2 bcrypt</h4>
<ul>
<li>功能：实现bcrypt(Blowfish-based password hashing function)密码哈希算法</li>
<li>用途：安全地存储密码</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/bcrypt"
hash, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
</code></pre>
<ul>
<li>使用建议：推荐使用，广泛应用于<a href="https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example/">密码存储</a>。</li>
</ul>
<h4>4.4.3 hkdf</h4>
<ul>
<li>功能：实现HMAC-based Key Derivation Function (HKDF)</li>
<li>用途：HKDF是基于HMAC（Hash-based Message Authentication Code）的一种变体，专门用于从较短的输入密钥材料（如共享密钥或密码）派生出更长的、安全的密钥。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/hkdf"
hkdf := hkdf.New(sha256.New, secret, salt, info)
</code></pre>
<ul>
<li>使用建议：推荐使用，是标准的密钥派生函数。</li>
</ul>
<h4>4.4.4 pbkdf2</h4>
<ul>
<li>功能：实现PBKDF2（Password-Based Key Derivation Function 2, 基于密码的密钥派生函数2）</li>
<li>用途：从密码派生密钥</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/pbkdf2"
dk := pbkdf2.Key([]byte("password"), salt, 4096, 32, sha1.New)
</code></pre>
<ul>
<li>使用建议：对于需要高安全性和抵抗暴力破解攻击的应用，PBKDF2是一个很好的选择。然而，对于更现代的应用，特别是那些对安全性有极高要求的应用，可能更推荐使用更现代的密码哈希算法，如Argon2。</li>
</ul>
<h4>4.4.5 scrypt</h4>
<ul>
<li>功能：实现scrypt(Scrypt key derivation function)密钥派生函数</li>
<li>用途：从密码派生密钥，特别适合抵抗硬件暴力破解</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/scrypt"
dk, _ := scrypt.Key([]byte("password"), salt, 32768, 8, 1, 32)
</code></pre>
<ul>
<li>使用建议：推荐使用，特别是在需要<a href="https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example/">抵抗硬件攻击或并行计算攻击的场景</a>。</li>
</ul>
<h3>4.5 公钥密码学</h3>
<h4>4.5.1 bn256</h4>
<ul>
<li>功能：实现256位Barreto-Naehrig曲线</li>
<li>用途：支持双线性对运算，用于某些高级密码协议</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/bn256"
g1 := new(bn256.G1).ScalarBaseMult(k)
</code></pre>
<ul>
<li>使用建议：该包已作废并冻结，不推荐使用。github.com/cloudflare/bn256有更完整的实现，但对于新的应用，特别是那些对安全性有极高要求的应用，不推荐使用bn256。</li>
</ul>
<h4>4.5.2 nacl</h4>
<ul>
<li>功能：提供NaCl（Networking and Cryptography library）的Go实现</li>
<li>用途：NaCl主要用于需要高效加密和安全通信的应用。它提供了各种加密原语，包括对称加密、公钥加密、哈希函数、消息认证码（MAC）和密钥协商协议等。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/nacl/box"
publicKey, privateKey, _ := box.GenerateKey(rand.Reader)
</code></pre>
<ul>
<li>使用建议：推荐使用，提供了易用的高级加密接口</li>
</ul>
<h3>4.6 协议和标准</h3>
<h4>4.6.1 acme</h4>
<ul>
<li>功能：实现ACME（Automatic Certificate Management Environment）协议，该协议旨在自动化证书的颁发、更新和管理。它允许服务器自动请求和接收TLS/SSL证书，而无需人工干预。</li>
<li>用途：自动化证书管理，如Let&#8217;s Encrypt</li>
<li>示例：使用较复杂，通常通过更高级的库如golang.org/x/crypto/acme/autocert使用，鉴于篇幅，这里就不贴代码了。</li>
<li>使用建议：在需要自动化证书管理的场景中推荐使用</li>
</ul>
<h4>4.6.2 ocsp</h4>
<ul>
<li>功能：实现在线证书状态协议（OCSP, Online Certificate Status Protocol），该协议提供了一种实时查询数字证书状态的方法。它允许客户端在建立安全连接之前，向证书颁发机构（CA）查询特定证书的有效性。</li>
<li>用途：检查X.509数字证书的撤销状态</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/ocsp"
resp, _ := ocsp.ParseResponse(responseBytes, issuer)
</code></pre>
<ul>
<li>使用建议：在需要证书状态检查的应用中推荐使用</li>
</ul>
<h4>4.6.3 openpgp</h4>
<ul>
<li>功能：实现OpenPGP(Open Pretty Good Privacy)标准。OpenPGP是一种加密标准，旨在提供数据加密和解密、数字签名和数据完整性保护。</li>
<li>用途：主要用于保护电子邮件通信、文件存储和数据传输的安全。它支持对称加密、公钥加密、哈希函数和消息认证码（MAC），以及生成和验证数字签名。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/openpgp"
entity, _ := openpgp.NewEntity("name", "comment", "email", nil)
</code></pre>
<ul>
<li>使用建议：OpenPGP是一个强大、灵活和安全的加密标准，被广泛用于各种安全协议和应用中，包括电子邮件加密、文件加密和数据传输加密。在许多现代安全应用中被推荐使用。</li>
</ul>
<h4>4.6.4 otr</h4>
<ul>
<li>功能：实现Off-The-Record Messaging (OTR) 离线消息传递协议</li>
<li>用途：提供即时通讯场景的端到端加密，确保通信内容只能被预期的接收者阅读，而不会被第三方窃听或篡改。</li>
<li>示例：（使用较复杂，通常需要结合具体的即时通讯应用）</li>
<li>使用建议：在开发加密即时通讯应用时可以考虑使用</li>
</ul>
<h4>4.6.5 pkcs12</h4>
<ul>
<li>功能：实现PKCS#12标准(Public-Key Cryptography Standards #12)，PKCS#12是由RSA Laboratories设计的，旨在定义一种标准格式，用于存储和传输私钥、公钥和证书链。PKCS#12文件通常以.p12或.pfx扩展名结尾。</li>
<li>用途：存储和传输服务器证书、中间证书和私钥</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/pkcs12"
blocks, _ := pkcs12.ToPEM(pfxData, "password")
</code></pre>
<ul>
<li>使用建议：PKCS#12是一个强大、安全和标准化的密钥和证书存储格式，在需要安全存储和传输加密密钥和证书的应用中被推荐使用。不过该包已经冻结，如需要，可考虑software.sslmate.com/src/go-pkcs12的实现(github.com/SSLMate/go-pkcs12)。</li>
</ul>
<h4>4.6.6 ssh</h4>
<ul>
<li>功能：实现SSH客户端和服务器</li>
<li>用途：提供安全的远程登录和其他安全网络服务</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/ssh"
config := &amp;ssh.ClientConfig{User: "user", Auth: []ssh.AuthMethod{ssh.Password("password")}}
</code></pre>
<ul>
<li>使用建议：强烈推荐用于实现SSH功能</li>
</ul>
<h3>4.7 其他</h3>
<h4>4.7.1 poly1305</h4>
<ul>
<li>功能：实现Poly1305消息认证码。Poly1305是一种高速的消息认证码（MAC）算法, 通常与ChaCha20流加密算法结合使用，形成ChaCha20-Poly1305组合，用于提供加密和消息认证的完整解决方案。</li>
<li>用途：用于消息认证，确保消息在传输过程中的完整性和真实性，未被篡改。</li>
<li>示例：</li>
</ul>
<pre><code>import "golang.org/x/crypto/poly1305"
var key [32]byte
var out [16]byte
poly1305.Sum(&amp;out, msg, &amp;key)
</code></pre>
<ul>
<li>使用建议：这个包的实现已作废，推荐使用golang.org/x/crypto/chacha20poly1305</li>
</ul>
<h2>5. Go密码学库的现状与后续方向</h2>
<p>Gotime在2023年末和今年年初对Go密码学库的前负责人Filippo Valsorda和现负责人Roland Shoemaker进行了三期访谈(见参考资料)，通过这三次访谈我们大约可以梳理出Go密码学库的现状与后续方向：</p>
<ul>
<li>RSA后端实现的改进，提高了安全性和性能。 </li>
<li>引入<a href="https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug">godebug机制</a>，允许在<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule">不破坏兼容性</a>的情况下逐步引入新的安全改进。</li>
<li>正在考虑对一些密码学包进行v2版本的设计，以提供更高级和更易用的API。</li>
<li>正在逐步弃用一些不安全的算法，如SHA1和MD5。</li>
<li>简化配置选项，减少用户需要做的选择，提供更多默认安全设置。</li>
<li>正在将golang.org/x/crypto中的重要包移入标准库，以减少混淆，包括继TLS之后的另外一个重要协议包ssh库。</li>
<li>使用BoringSSL的BoGo测试套件来全面测试Go的TLS实现。 </li>
<li>Go密码学库正在实现这些新的<a href="https://en.wikipedia.org/wiki/Post-quantum_cryptography">后量子密码算法</a>，但目前还没有完全集成到标准库中。</li>
</ul>
<p>总的来说，Go密码学库(包括golang.org/x/crypto)正在积极发展和改进，同时也在为后量子密码学时代做准备。虽然后量子算法的完全集成和广泛应用还需要一段时间，但Go团队正在积极跟进这一领域的发展，努力在保持兼容性的同时提升安全性和性能。</p>
<h2>6. 小结</h2>
<p>在这篇文章中，我们对Go生态中密码学功能的核心：Go crypto库(包括标准库crypto相关包以及golang.org/x/crypto相关包)进行了全面的了解，包括两者的关系、整体结构设计原则以及每个库的子包概览。</p>
<p>我们看到：Go crypto库以其安全性、全面性、易用性、高性能以及与Go生态系统的高度集成而著称。它不仅涵盖了广泛的加密算法和协议，还通过统一且直观的API降低了使用门槛。</p>
<p>相信通过上述的了解，大家都已经理解了Go crypto库的架构与设计思想，并建立起了一张crypto库的“地图”。按照这幅图的指示，大家可以根据具体需求，快速找到合适的密码学包，并利用这些包构建安全可靠的Go应用。</p>
<h2>7. 参考资料</h2>
<ul>
<li><a href="https://changelog.com/gotime/295">What&#8217;s new in Go&#8217;s cryptography libraries: Part 1</a> &#8211; https://changelog.com/gotime/295</li>
<li><a href="https://changelog.com/gotime/298">What&#8217;s new in Go&#8217;s cryptography libraries: Part 2</a> &#8211; https://changelog.com/gotime/298</li>
<li><a href="https://changelog.com/gotime/313?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 3</a> &#8211; https://changelog.com/gotime/313</li>
<li><a href="https://cyberspy.io/articles/crypto101/">Crypto 101: A Brief Tour of Practical Crypto in Golang</a> &#8211; https://cyberspy.io/articles/crypto101/</li>
<li><a href="https://www.youtube.com/watch?v=2r_KMzXB74w">Go for Crypto Developers</a> &#8211; https://www.youtube.com/watch?v=2r_KMzXB74w </li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解Web应用的机密管理</title>
		<link>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/</link>
		<comments>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/#comments</comments>
		<pubDate>Tue, 07 Nov 2023 21:58:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ACL]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Barrier]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[credentials]]></category>
		<category><![CDATA[CRUD]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[HSM]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[jwt]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[KeePass]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[KeyWhiz]]></category>
		<category><![CDATA[KMS]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[OTP]]></category>
		<category><![CDATA[rotate]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[sealed]]></category>
		<category><![CDATA[secret]]></category>
		<category><![CDATA[secret-management]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[unseal]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[一次性密码]]></category>
		<category><![CDATA[令牌]]></category>
		<category><![CDATA[凭据]]></category>
		<category><![CDATA[凭证]]></category>
		<category><![CDATA[密码]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[授权]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[日志]]></category>
		<category><![CDATA[明文]]></category>
		<category><![CDATA[机密管理]]></category>
		<category><![CDATA[身份认证]]></category>
		<category><![CDATA[配置文件]]></category>
		<category><![CDATA[高可用]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4037</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example 如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着CRUD的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？ 你只是大多应用系统开发人员中的一个典型代表。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现： 明文存储密码、密钥等敏感数据 很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。 硬编码密钥与密码 重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。 使用弱密码、未定期更换 使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。 不同环境复用同一密钥 开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。 明文传输密码 HTTP传输中明文传递密码，导致可被嗅探截获。 日志中输出明文密码 调试日志中直接输出数据库密码等敏感信息，可能被利用。 缺乏访问控制和审计机制 密钥等资源无访问控制，且操作不被审计，难以追踪。 这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。 在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。 1. 认识机密管理 在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。 机密管理拥有一套自己的核心管理措施，包括： 从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储; 执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限； 执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计； 定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)； 自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略； &#8230; &#8230; 机密管理涉及要管理的机密信息的类型包括(但不限于)： 用户密码或自动生成的密码 API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证） SSH密钥 数据库和其他system-to-system的密码 用于安全通信、传输和接收数据的私人证书（TLS、SSL 等） RSA和其他一次性密码设备 综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的机密管理系统来应对。 2. 机密管理系统 机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段： 文件加密阶段 早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。 自建解决方案阶段 企业开始自研一些机密管理解决方案(包括基于一些像KeePass这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。 开源机密管理项目 随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：Vault、Keywhiz等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。 注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example</p>
<p>如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着<a href="https://www.codecademy.com/article/what-is-crud">CRUD</a>的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？</p>
<p>你只是大多应用系统开发人员中的一个<strong>典型代表</strong>。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现：</p>
<ul>
<li>明文存储密码、密钥等敏感数据</li>
</ul>
<p>很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。</p>
<ul>
<li>硬编码密钥与密码</li>
</ul>
<p>重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。</p>
<ul>
<li>使用弱密码、未定期更换</li>
</ul>
<p>使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。</p>
<ul>
<li>不同环境复用同一密钥</li>
</ul>
<p>开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。</p>
<ul>
<li>明文传输密码</li>
</ul>
<p>HTTP传输中明文传递密码，导致可被嗅探截获。</p>
<ul>
<li>日志中输出明文密码</li>
</ul>
<p>调试日志中直接输出数据库密码等敏感信息，可能被利用。</p>
<ul>
<li>缺乏访问控制和审计机制</li>
</ul>
<p>密钥等资源无访问控制，且操作不被审计，难以追踪。</p>
<p>这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。</p>
<p>在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。</p>
<h2>1. 认识机密管理</h2>
<p>在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。</p>
<p>机密管理拥有一套自己的核心管理措施，包括：</p>
<ul>
<li>从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储;</li>
<li>执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限；</li>
<li>执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计；</li>
<li>定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)；</li>
<li>自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略；</li>
<li>&#8230; &#8230;</li>
</ul>
<p>机密管理涉及要管理的机密信息的类型包括(但不限于)：</p>
<ul>
<li>用户密码或自动生成的密码</li>
<li>API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证）</li>
<li>SSH密钥</li>
<li>数据库和其他system-to-system的密码</li>
<li>用于安全通信、传输和接收数据的私人证书（TLS、SSL 等）</li>
<li>RSA和其他一次性密码设备</li>
</ul>
<p>综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的<strong>机密管理系统</strong>来应对。</p>
<h2>2. 机密管理系统</h2>
<p>机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段：</p>
<ul>
<li>文件加密阶段</li>
</ul>
<p>早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。</p>
<ul>
<li>自建解决方案阶段</li>
</ul>
<p>企业开始自研一些机密管理解决方案(包括基于一些像<a href="https://keepass.info">KeePass</a>这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。</p>
<ul>
<li>开源机密管理项目</li>
</ul>
<p>随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：<a href="https://github.com/hashicorp/vault/">Vault</a>、<a href="https://github.com/square/keywhiz">Keywhiz</a>等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。</p>
<blockquote>
<p>注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp Vault。</p>
</blockquote>
<ul>
<li>云原生机密管理平台</li>
</ul>
<p>在开源机密管理项目的基础之上，这些开源项目背后的开发商以及一些专业的公有云提供商开始面向云原生应用和DevOps，以SaaS形式提供专业的机密管理服务和全面的机密管理解决方案，如<a href="https://azure.microsoft.com/en-us/products/key-vault/">Azure Key Vault</a>、<a href="https://cloud.google.com/secret-manager">Google Secret Manager</a>、<a href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a>、HashiCorp Vault等。</p>
<p>我们看到：专业的机密系统发展到今天的水平，其过程不是一蹴而就的。正是基于历史经验的积累和总结，现代机密管理平台才演化出了面向云原生架构、支持DevOps、细粒度访问控制、机密信息的动态化以及生命周期的自动化管理等先进功能特性。</p>
<p>在上面的优秀的云原生机密管理系统中，HashiCorp Vault是唯一开源且可以私有化部署在企业内部的。<a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp公司于2015年发布并开源了Vault</a>，经过多年发展，Vault已经发展成为一款功能强大的企业级机密管理系统，并被广泛视为云原生领域的首选解决方案。</p>
<p>对于普通Web应用开发者而言，<strong>既要有机密管理的意识，又要有机密管理的实现手段</strong>。HashiCorp Vault的设计目标之一就是将机密管理下沉到平台层面，让应用开发者能够专注于应用程序的开发而无需过多关注机密的管理和保护。</p>
<p>作为Web应用开发者，基于Vault实现Web应用的机密管理是一条非常可行的机密管理方案。通过与Vault的集成，Web应用开发者可以利用Vault提供的丰富功能来处理各种机密管理需求和场景。开发者只需要学习如何使用Vault的API或客户端库与Vault进行交互，就能轻松地访问和管理机密数据，实现机密信息(如数据库凭据、API 密钥等)获取、动态机密信息生成、访问控制、审计和监控等机密管理功能，并且可以减少机密管理的开发和维护的复杂性。</p>
<p>接下来，我就和大家一起简要的了解一下Hashicorp的Vault。</p>
<h2>3. 认识Vault</h2>
<h3>3.1 Vault的架构</h3>
<p>如果对Hashicorp这家公司很熟悉，你肯定知道Hashicorp大部分产品(和开源项目)都是由Go开发的，包括<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">consul</a>、<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">nomad</a>、<a href="https://github.com/hashicorp/terraform">terraform</a>以及<a href="https://github.com/hashicorp/vagrant">vagrant</a>(<a href="https://discuss.hashicorp.com/t/status-of-vagrant-go/42562">vagrant的新版本将切换到go实现</a>)等。</p>
<p>Vault这款优秀的机密管理软件系统继承了Hashicorp的开发基因，也是由Go语言开发的。从2015年至今，Vault已经演化为一个功能强大，但相对也比较复杂的系统，下面是<a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Hashicorp官方架构文档</a>中的一个关于Vault的high level的结构示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-2.png" alt="" /></p>
<p>从整体架构设计思路来看，vault支持：</p>
<ul>
<li>高可用性</li>
</ul>
<p>Vault的架构设计允许部署多个Vault服务器以实现高可用性和容错性，在<a href="https://developer.hashicorp.com/vault/docs/internals/high-availability">高可用集群部署模式</a>下，多个vault服务器共享存储后端，并且每个vault服务器可能是两个状态：active和standby。任意时刻集群都只有一个实例处于active状态，所有standby实例都处于热备用状态(hot standby)。只有处于active状态的服务器会处理所有请求；standby服务器会将所有请求重定向到活动Vault服务器，这点与consul的设计是一致的。如果active服务器被sealed、发生故障或失去网络连接，则standby Vault服务器中的一个将成为active实例。</p>
<p>这里有人可能会问：如果只有一个active实例，那么在访问量增大的时候，active实例便会成为热点或性能瓶颈！没错，这是vault开源版本的约束。这个约束在vault的企业付费版中被取消，在付费版中，standby服务器可以接收只读请求，所有只读请求会均衡分担到各个standby实例上，如果standby实例收到写请求，它会将写请求转发给active实例处理。</p>
<ul>
<li>封存和解封</li>
</ul>
<p>说高可用性时，我们提到了vault服务器实例的sealed(封存)状态。启动Vault服务器时，它会处于sealed状态。在这种状态下，Vault仅知道访问物理存储的位置和方式，但不知道如何解密存储中数据。在unseal(解封)之前，该vault服务器几乎无法做任何操作。在对处于sealed状态的Vault实例进行任何操作之前，必须对其进行解封(unseal)。</p>
<p>解封操作需要提供<strong>解封密钥(unseal keys)</strong>。有人注意到了，我用了unseal keys，而不是unseal key，因为解封密钥是由一种名为<a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir&#8217;s Secret Sharing</a>的算法分解保存和汇集生成的。Shamir&#8217;s Secret Sharing（Shamir的机密分享算法）是一种密码学算法，用于将机密数据(在本文中指的就是“unseal key”)分割成多个部分，称为shares。这些share可以被分发给不同的人，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-3.png" alt="" /></p>
<p>而只有当足够数量的share被汇集时，才能恢复出原始的机密数据(unseal key)，并用恢复出的机密数据进行下一步操作(如下图所示，下图来自Hashicorp官方文档)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-4.png" alt="" /></p>
<p>在这幅图中，当汇集一定个数的unseal keys&#8217;share后，vault就能够重构解封密钥(“unseal key”)，然后用它来解密得到根密钥(root key，也称为master key)，根密钥再被用来解密得到加密密钥(Encryption key)用于保护所有vault的数据，即这个Encryption key就是后续参与机密数据加解密的密钥。</p>
<blockquote>
<p>注：实际生产部署时，究竟要如何对Vault Server进行unseal，<a href="https://developer.hashicorp.com/vault/tutorials/recommended-patterns/pattern-unseal">HashiCorp提供了一些unseal pattern</a>供大家参考。</p>
</blockquote>
<ul>
<li>加密层</li>
</ul>
<p>前面架构图中左侧南北横贯多层的部分是Vault的加密层，被称为<strong>barrier</strong>，负责对Vault数据进行加密和解密，确保数据在存储和传输过程中的机密性和完整性。Vault服务器启动时，会将数据写入存储后端。由于存储后端位于barrier之外，被视为不可信的(与零信任网络理念一致)，因此Vault会在将数据发送到存储后端之前对其进行加密。这种机制确保了即便恶意攻击者试获取了对存储后端的访问权限，其拿到的数据仍然保持加密状态。</p>
<ul>
<li>认证和授权</li>
</ul>
<p>如下图(来自Hashicorp官方文档)，当客户端首次连接到Vault时，需要进行身份验证。Vault提供可配置的认证方法，并在身份验证机制上提供灵活性。操作员可以使用用户名/密码等机制进行身份验证，而应用程序可以使用公钥/私钥或令牌进行身份验证。 Core（核心）负责管理请求的流程，包括流经哪个身份验证方法来确定请求是否有效，并得到关联策略的列表，执行访问控制规则（ACLs），确保审计日志记录，并将请求路由到相应的机密引擎进行处理。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-5.png" alt="" /></p>
<ul>
<li>策略管理</li>
</ul>
<p>策略是一组命名的访问控制规则。Vault内置了一些策略，如”root”策略，允许对所有资源的访问。用户可以创建任意数量的命名策略，并对路径进行细粒度的控制。除非通过策略明确授权，否则不允许进行操作。</p>
<ul>
<li>机密引擎</li>
</ul>
<p>Vault使用机密引擎来生成和管理动态机密数据，如临时凭据、API密钥等。机密引擎的类型可以是静态的，如数据库凭据，也可以是动态的，如 AWS IAM凭据。机密引擎根据配置的规则和策略生成和提供机密数据。</p>
<ul>
<li>审计和日志记录</li>
</ul>
<p>Vault记录请求和响应的审计日志，并有Audit Broker(审计代理)将其分发到配置的审计设备(audit device)。审计日志用于监控和审计对Vault的访问和操作。</p>
<ul>
<li>Expiration Manager（租期管理）</li>
</ul>
<p>Vault由Expiration Mgr管理令牌和机密数据的过期，自动回收已过期的客户端令牌和机密数据。</p>
<ul>
<li>Token Store（令牌存储）</li>
</ul>
<p>Token Store生成和管理客户端令牌，用于进行后续的请求操作。令牌类似于网站登录时发送的 cookie，用于验证客户端的身份和授权。</p>
<p>以上是Vault的主要架构设计思路和各部分的功能范围。Vault的架构保证了安全性、高可用性和可扩展性，使用户能够安全地管理和保护机密信息。</p>
<h3>3.2 Vault的安全模型</h3>
<p>Vault是做机密信息管理的，其自身安全模型是否完善直接关系到应用系统的安全。Vault官方也十分重视这点，在<a href="https://developer.hashicorp.com/vault/docs/internals/security">官方文档中也对其安全模型做了说明</a>，这里梳理一下。</p>
<p>Vault的安全模型旨在提供数据的机密性、完整性、可用性、可追溯性和认证性。以下是Vault安全模型的几个设计要点：</p>
<ul>
<li>通信安全</li>
</ul>
<p>Vault要求客户端与服务器之间的通信通过TLS建立安全通道，以确保通信的机密性和完整性。此外，Vault服务器之间的集群通信也使用相互认证的TLS，以保护数据在传输过程中的机密性和完整性。</p>
<ul>
<li>身份验证和授权</li>
</ul>
<p>前面说架构时提及过：所有客户端请求必须经过适当的身份验证和授权。当客户端首次进行身份验证时，Vault使用认证方法验证客户端的身份，并返回与其关联的ACL策略列表。每个请求都需要提供有效的客户端令牌，Vault根据令牌验证其有效性，并生成基于关联策略的访问控制列表（ACL）。</p>
<ul>
<li>数据安全</li>
</ul>
<p>Vault对于存储在后端的数据，以及在传输过程中的数据，都要求保证安全。Vault使用256位的高级加密标准（AES）密码和96位随机数作为加密密钥，对离开Vault的所有数据进行加密。同时，在解密过程中验证Galios Counter Mode（GCM）的认证标签，以检测任何篡改。</p>
<ul>
<li>内部威胁保护</li>
</ul>
<p>Vault关注内部攻击威胁，即已经获得某种程度Vault访问权限的攻击者企图获取未经授权的机密信息。Vault在客户端进行身份验证时，使用事先配置的关联策略列表来生成客户端令牌，并使用严格的默认拒绝策略来进行访问控制。每个策略指定对Vault中路径的访问级别，最终的访问权限由所有关联策略中最高级别的权限决定。</p>
<ul>
<li>密钥管理</li>
</ul>
<p>Vault使用Shamir&#8217;s Secret Sharing技术来实现密钥的管理和保护unseal key，本质上也是对Root key和Encryption key的保护。只有在提供足够数量的share时，才能恢复unseal密钥，这样可以避免对单个持有者的绝对信任，同时也不需要存储完整的加密密钥。</p>
<p>但需要注意的是，Vault的安全模型并不涵盖所有可能的威胁和攻击，例如对存储后端的完全控制、存储后端中存在的秘密信息的泄露、运行中的Vault实例内存分析等。此外，Vault还依赖于外部系统或服务的安全性，如果这些外部系统存在漏洞或受到攻击，可能会导致Vault中数据的机密性或完整性受到威胁。</p>
<p>说了这么多Vault，Vault究竟长什么样？应该如何用呢？接下来我们简单介绍一下Vault的安装和使用，也是为后续的实例部分做个铺垫。</p>
<h3>3.3 Vault的安装</h3>
<p><a href="https://developer.hashicorp.com/vault/docs/install">Vault支持多种形式的安装部署</a>，包括基于预编译好的二进制文件(precompiled binary)、基于容器或包管理器等，你甚至可以自己基于源码编译。</p>
<p>我这里使用的是Precompiled binary方式，将Vault直接部署在我的开发环境下，一台MacBook Pro上。</p>
<p>Precompiled binary下载后就是一个可执行文件，把它放到特定路径下，并在PATH环境变量中将这个路径加入进来，环境变量生效后，你就可以在任意路径下使用vault命令了。</p>
<p>下面的命令打印了下载的vault的版本：</p>
<pre><code>$vault -v
Vault v1.15.1 (b94e275f25ccd9011146d14c00ea9e49fd5032dc), built 2023-10-20T19:16:11Z
</code></pre>
<p>通过-h命令行参数，可以查看vault的命令帮助信息：</p>
<pre><code>$vault -h
Usage: vault &lt;command&gt; [args]

Common commands:
    read        Read data and retrieves secrets
    write       Write data, configuration, and secrets
    delete      Delete secrets and configuration
    list        List data or secrets
    login       Authenticate locally
    agent       Start a Vault agent
    server      Start a Vault server
    status      Print seal and HA status
    unwrap      Unwrap a wrapped secret

Other commands:
    audit                Interact with audit devices
    auth                 Interact with auth methods
    debug                Runs the debug command
    events
    kv                   Interact with Vault's Key-Value storage
    lease                Interact with leases
    monitor              Stream log messages from a Vault server
    namespace            Interact with namespaces
    operator             Perform operator-specific tasks
    patch                Patch data, configuration, and secrets
    path-help            Retrieve API help for paths
    pki                  Interact with Vault's PKI Secrets Engine
    plugin               Interact with Vault plugins and catalog
    policy               Interact with policies
    print                Prints runtime configurations
    proxy                Start a Vault Proxy
    secrets              Interact with secrets engines
    ssh                  Initiate an SSH session
    token                Interact with tokens
    transform            Interact with Vault's Transform Secrets Engine
    transit              Interact with Vault's Transit Secrets Engine
    version-history      Prints the version history of the target Vault server
</code></pre>
<blockquote>
<p>注：Vault继承了Hashicorp产品的一贯风格，即将所有功能放到一个程序中，各个功能通过subcommand的形式提供，比如vault server、vault agent、vault proxy等。如果你了解consul，你会发现consul就是这样的。</p>
</blockquote>
<h3>3.4 Vault的启动(dev模式)</h3>
<p>生产环境的Vault部署、配置、启动以及unseal过程还是蛮复杂的，<a href="https://developer.hashicorp.com/vault/tutorials/day-one-consul/multi-cluster-architecture">HashiCorp给了一些参考集群架构</a>，这些可以交给运维同学去琢磨。</p>
<p>对于开发人员而言，日常将应用与Vault集成实现机密管理的时候，只需在本机或远程开发机上启动dev模式的Vault实例即可，这里我们也基于dev模式来启动一个单实例的Vault：</p>
<pre><code>$vault server -dev
==&gt; Vault server configuration:

Administrative Namespace:
             Api Address: http://127.0.0.1:8200
                     Cgo: disabled
         Cluster Address: https://127.0.0.1:8201
   Environment Variables: Apple_PubSub_Socket_Render, CLASSPATH, CLISH_PATH, ETCDCTL_API, GITEA_WORK_DIR, GODEBUG, GONOPROXY, GONOSUMDB, GOPATH, GOPRIVATE, GOPROXY, GOROOT, GOSUMDB, HOME, HOMEBREW_BOTTLE_DOMAIN, LANG, LC_CTYPE, LESS, LOGNAME, LSCOLORS, MML_HOME, NVM_BIN, NVM_CD_FLAGS, NVM_DIR, OLDPWD, OPENCV_PATH, PAGER, PATH, PWD, PYTHONPATH, RUSTUP_DIST_SERVER, RUSTUP_UPDATE_ROOT, SHELL, SHLVL, SSH_AUTH_SOCK, TERM, TERM_PROGRAM, TERM_PROGRAM_VERSION, TERM_SESSION_ID, TMPDIR, USER, XPC_FLAGS, XPC_SERVICE_NAME, ZSH, _
              Go Version: go1.21.3
              Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level:
                   Mlock: supported: false, enabled: false
           Recovery Mode: false
                 Storage: inmem
                 Version: Vault v1.15.1, built 2023-10-20T19:16:11Z
             Version Sha: b94e275f25ccd9011146d14c00ea9e49fd5032dc

==&gt; Vault server started! Log data will stream in below:

2023-11-06T10:25:37.723+0800 [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
2023-11-06T10:25:37.727+0800 [INFO]  incrementing seal generation: generation=1
2023-11-06T10:25:37.727+0800 [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2023-11-06T10:25:37.733+0800 [INFO]  core: Initializing version history cache for core
2023-11-06T10:25:37.734+0800 [INFO]  events: Starting event system
2023-11-06T10:25:37.736+0800 [INFO]  core: security barrier not initialized
2023-11-06T10:25:37.737+0800 [INFO]  core: security barrier initialized: stored=1 shares=1 threshold=1
2023-11-06T10:25:37.744+0800 [INFO]  core: post-unseal setup starting
2023-11-06T10:25:37.758+0800 [INFO]  core: loaded wrapping token key
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T10:25:37.760+0800 [INFO]  core: no mounts; adding default mount table
2023-11-06T10:25:37.765+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T10:25:37.774+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T10:25:37.777+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T10:25:37.783+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T10:25:37.785+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T10:25:37.787+0800 [INFO]  rollback: starting rollback manager
2023-11-06T10:25:37.789+0800 [INFO]  core: restoring leases
2023-11-06T10:25:37.791+0800 [INFO]  identity: entities restored
2023-11-06T10:25:37.791+0800 [INFO]  identity: groups restored
2023-11-06T10:25:37.791+0800 [INFO]  expiration: lease restore complete
2023-11-06T10:25:37.793+0800 [INFO]  core: Recorded vault version: vault version=1.15.1 upgrade time="2023-11-06 02:25:37.793171 +0000 UTC" build date=2023-10-20T19:16:11Z
2023-11-06T22:25:38.367+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.368+0800 [INFO]  core: root token generated
2023-11-06T22:25:38.368+0800 [INFO]  core: pre-seal teardown starting
2023-11-06T22:25:38.369+0800 [INFO]  rollback: stopping rollback manager
2023-11-06T22:25:38.369+0800 [INFO]  core: pre-seal teardown complete
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener.tcp: starting listener: listener_address=127.0.0.1:8201
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener: serving cluster requests: cluster_listen_address=127.0.0.1:8201
2023-11-06T22:25:38.371+0800 [INFO]  core: post-unseal setup starting
2023-11-06T22:25:38.371+0800 [INFO]  core: loaded wrapping token key
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T22:25:38.373+0800 [INFO]  rollback: starting rollback manager
2023-11-06T22:25:38.374+0800 [INFO]  core: restoring leases
2023-11-06T22:25:38.374+0800 [INFO]  expiration: lease restore complete
2023-11-06T22:25:38.374+0800 [INFO]  identity: entities restored
2023-11-06T22:25:38.374+0800 [INFO]  identity: groups restored
2023-11-06T22:25:38.374+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.374+0800 [INFO]  core: vault is unsealed
2023-11-06T22:25:38.386+0800 [INFO]  core: successful mount: namespace="" path=secret/ type=kv version=""
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variables:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: KiF1ohtchsOjr4IvzHY38/OAPOqS1/rARczTFG6Ull8=
Root Token: hvs.9QOJsa7zlwHO8ieW15CXXoOp

Development mode should NOT be used in production installations!
</code></pre>
<p>我们看到dev模式下，Vault server是自动unseal的，并打印出了Unseal Key和Root Token，而且显式地告诉你：所有机密数据都是存储在内存中的，<strong>不要将这个模式用于生产环境</strong>。</p>
<p>前面说过，vault程序继承了Hashicorp产品的基因，它既可以用来启动server，其自身也是一个命令行程序，我们可以用vault命令查看启动的server的状态：</p>
<pre><code>$vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": http: server gave HTTP response to HTTPS client
</code></pre>
<p>我们看到：获取vault server状态的命令执行失败，因为我们并没有开启vault server的https端口，仅使用了http端口。我们设置一下环境变量后，再执行status命令：</p>
<pre><code>$export VAULT_ADDR='http://127.0.0.1:8200' // 设置vault server addr为http非安全方式
$vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.15.1
Build Date      2023-10-20T19:16:11Z
Storage Type    inmem
Cluster Name    vault-cluster-23f54192
Cluster ID      a86c14e2-b88c-5391-e8b4-0b1b9e9a9aaf
HA Enabled      false
</code></pre>
<p>接下来，我们试着向Vault写入一个机密信息。Vault支持多种secret engine，比如：<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/static-secrets">Key/Value secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/versioned-kv">Versioned Key/value secrets engine(k/v引擎的v2版本)</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/openldap">LDAP secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/azure-secrets">Azure secrets engine</a>等，其中K/V引擎以及带版本的K/V引擎是最常用的。</p>
<blockquote>
<p>注：Vault还支持开发者<a href="https://developer.hashicorp.com/vault/tutorials/custom-secrets-engine">自定义secret engine</a>。</p>
</blockquote>
<p>我们尝试使用kv子命令向vault中写入一个key/value，放到secret路径下(在dev模式下，secret路径下自动开启v2版本引擎)，key为hello，值为foo=world：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
Error making API request.

URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret
Code: 403. Errors:

* permission denied

</code></pre>
<p>我们看到命令执行失败，提示没有权限。vault server要求每个访问请求都必须带上token，我们可以使用vault server启动时打印的root token，可以使用环境变量的方式将token注入：</p>
<pre><code>export VAULT_TOKEN="hvs.9QOJsa7zlwHO8ieW15CXXoOp"
</code></pre>
<p>也可以执行下面命令并输入root token完成登录：</p>
<pre><code>$vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.9QOJsa7zlwHO8ieW15CXXoOp
token_accessor       170OHOscEZjfl8fSa8aVpNkZ
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
</code></pre>
<p>之后，root token就被放置在“~/.vault-token”中了：</p>
<pre><code>$cat ~/.vault-token
hvs.9QOJsa7zlwHO8ieW15CXXoOp
</code></pre>
<blockquote>
<p>注：我们通常不会使用root token，而是会<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/vault-nomad-secrets">利用vault token命令生成新token</a>作为vault cli访问vault server的token。</p>
</blockquote>
<p>现在我们重新执行一下kv put命令：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2
</code></pre>
<p>kv创建成功，路径secret/data/hello(注：vault会默认在mount的路径secret下创建data路径)。vault server在将value值存储在backend storage(这里是memory)前，会用Encryption Key对内容进行加密。如果你多执行几次这个命令，你会发现输出信息中的version的数值会递增，这个数值表示设置的值的版本。</p>
<p>我们可以用kv get获取刚才写入的kv值，vault会将数据从backend storage中读取出来并解密：</p>
<pre><code>$vault kv get -mount=secret hello
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2

=== Data ===
Key    Value
---    -----
foo    world
</code></pre>
<p>我们还可以通过delete删除刚刚建立的kv值(为后面的基本场景示例做铺垫)：</p>
<pre><code>$vault kv delete secret/foo
Success! Data deleted (if it existed) at: secret/data/foo
$vault kv get secret/foo
No value found at secret/data/foo
</code></pre>
<p>到这里我们看到，一旦vault安装完毕后，基本使用场景还是蛮简单的，但也仅限于基本使用场景^_^。下面我们再来看看如何通过代码来实现这些基本功能场景。</p>
<h3>3.5 使用client SDK与Vault交互</h3>
<p><a href="https://developer.hashicorp.com/vault/api-docs/libraries">Vault支持各种主流语言的client SDK</a>，其中Vault官方维护了三个：Go、Ruby和C&#35;，其他语言的SDK则是由社区维护。</p>
<p>我们用Go Client SDK来编写一个设置kv和获取kv值的小程序，如下面代码所示：</p>
<pre><code>// secret-management-examples/basic/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
)

func main() {
    // 创建一个新的Vault客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Println("无法创建Vault客户端:", err)
        return
    }

    // 设置Vault服务器的地址
    client.SetAddress("http://localhost:8200/")

    // 设置Vault的访问令牌（如果需要认证）
    client.SetToken("hvs.9QOJsa7zlwHO8ieW15CXXoOp")

    // 设置要写入的机密信息
    secretData := map[string]interface{}{
        "foo": "bar",
    }

    kv2 := client.KVv2("secret") // mount "secret"

    // 写入机密信息到Vault的secret/data/{key}路径下
    key := "hello"
    _, err = kv2.Put(context.Background(), key, secretData)
    if err != nil {
        fmt.Println("无法写入机密信息:", err)
        return
    }

    // 读取Vault的secret/data/{key}路径下的机密信息
    secret, err := kv2.Get(context.Background(), key)
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", secret.Data)
}
</code></pre>
<p>我们看到：默认创建的api.Client操作的都是v1版本的数据，这里通过KVv2方法将其转换为可以操作v2版本数据的client，之后put和get就可以如预期正常工作了！</p>
<p>下面是其运行结果：</p>
<pre><code>$go run main.go
读取到的值: map[foo:bar]
</code></pre>
<p>有了基础场景做铺垫，接下来我们就进入实例环节，看看应用是如何基于Vault应对一些常见的机密管理场景的。</p>
<h2>4. 常见的机密管理场景</h2>
<p>Vault支持对多种机密信息的管理，包括应用访问外部服务或资源所需的用户名/密码、API密钥或访问令牌(token)，应用程序的配置中的机密配置信息，比如数据库连接字符串、加密密钥等，以及私钥、证书等加密相关的机密信息等。这里我们就分别来看看应用与Vault集成并获取这些机密信息的场景，不过在这之前，我们首先需要先来了解一下应用本身与Vault是如何集成的。</p>
<h3>4.1 应用通过Vault身份认证和授权的方法</h3>
<p>在3.5小节的基本场景示例中，我们的client使用了<a href="https://developer.hashicorp.com/vault/docs/auth/token">一个长期有效的token</a>通过了Vault的身份认证和授权环节，拥有了操作Vault数据的权限。</p>
<p>token auth方法也是dev模式下Vault server实例支持的唯一auth method，我们可以通过auth list命令查看vault server当前支持的auth方法集合：</p>
<pre><code>$vault auth list
Path      Type     Accessor               Description                Version
----      ----     --------               -----------                -------
token/    token    auth_token_6f9cc41c    token based credentials    n/a
</code></pre>
<p>不过，基于token来实现app与Vault的集成并非Vault官方推荐的在生产环境使用的<a href="https://developer.hashicorp.com/vault/docs/auth">auth方式</a>，理由也很明显：这种方式涉及手动创建一个长期有效的令牌，这有悖于最佳实践，并存在安全风险。</p>
<p>除了Token auth method，Vault还支持<a href="https://developer.hashicorp.com/vault/docs/auth/approle">AppRole</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/jwt">JWT/OIDC</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/cert">TLS证书</a>以及<a href="https://developer.hashicorp.com/vault/docs/auth/userpass">User/Password</a>等<a href="https://developer.hashicorp.com/vault/docs/auth">多种auth method</a>，这些auth method的共同之处在于通过身份认证后，Vault可自动创建<strong>短期令牌</strong>供客户端使用，无需定期手动生成新令牌，短期令牌可以减少令牌泄露的风险，因为短期令牌在一定时间后会自动失效，并需要重新进行身份认证。</p>
<p>简单起见，我这里就用User/Password method作为实例演示一下应用通过Vault的身份认证和授权。</p>
<p>我们先来开启(enable)基于User/Password的auth method：</p>
<pre><code>$vault auth enable userpass
Success! Enabled userpass auth method at: userpass/
</code></pre>
<p>该命令默认将会启用auth/userpass路径，之后通过auth list查看，就能在list中看到新增的userpass auth method了：</p>
<pre><code>$vault auth list
Path         Type        Accessor                  Description                Version
----         ----        --------                  -----------                -------
token/       token       auth_token_6f9cc41c       token based credentials    n/a
userpass/    userpass    auth_userpass_b5b6e974    n/a                        n/a

</code></pre>
<p>接下来，我们在vault服务实例中建立一个新的user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [default]
token_ttl                  0s
token_type                 default
</code></pre>
<p>下面是示例代码：</p>
<pre><code>// secret-management-examples/auth_user_password/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>如果你在Vault的GO SDK中没有找到对user/password auth method的直接支持，你也可以参考<a href="https://developer.hashicorp.com/vault/api-docs/auth/userpass">user/password auth method的API文档</a>自行实现登录Vault并读取特定机密信息，代码如下(与上面代码功能是等价的)：</p>
<pre><code>// secret-management-examples/auth_user_password_self_impl/main.go

func clientAuth(vaultAddr, user, pass string) (*api.Secret, error) {
    payload := fmt.Sprintf(`{"password": "%s"}`, pass)

    req, err := http.NewRequest("POST", vaultAddr+"/v1/auth/userpass/login/"+user, strings.NewReader(payload))
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New(string(body))
    }

    return api.ParseSecret(bytes.NewReader(body))
}

func main() {
    vaultAddr := "http://localhost:8200"
    user := "tonybai"
    pass := "ilovegolang"

    // client登录vault服务器获取临时访问令牌
    secret, err := clientAuth(vaultAddr, user, pass)
    if err != nil {
        fmt.Printf("登录Vault失败: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }

    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")
    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>我们运行一下上述两个示例代码之一：</p>
<pre><code>$go run main.go
登录Vault成功
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* 1 error occurred:
    * permission denied

</code></pre>
<p>通过错误信息来看，“tonybai”这个user没有权限读取secret/data/hello下的机密信息！那么怎么给这个用户加上secret/data/hello的读取权限呢？Vault通过policy来管理权限，如果某个user具有某个policy的绑定，那么该user就拥有该policy设定的权限，这有点像RBAC的思路，只是没有引入role的概念! 我们先来添加一个拥有secret/data/hello读权限的policy：</p>
<pre><code>$vault policy write my-policy -&lt;&lt;EOF
# Allow "read" permission on "secret/data/*" secrets
path "secret/data/*" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: my-policy
</code></pre>
<p>接下来重写user的属性数据，将my-policy赋给”tonybai”这个user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  0s
token_type                 default
</code></pre>
<p>完成上述设置后，我们再来运行一下基于user/password auth method的程序：</p>
<pre><code>$go run main.go
登录Vault成功
读取到的值: map[foo:bar]
</code></pre>
<p>这次程序成功登录Vault并成功读取了secret/data/hello下面的机密数据。</p>
<p>这里我们除了设置了token_policies，其他属性都保持了默认值，这样我们拿到的临时token其实并不“临时”，我们可以一直使用。下面我们通过设置token_ttl来指定每个临时token的最大有效时间：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy token_ttl=5s
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  5s
token_type                 default
</code></pre>
<p>我们改写一下程序，让程序每隔1秒用临时token获取一下机密信息并输出：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go (临时版本)

    for {
        // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
        data, err := kv2.Get(context.Background(), "hello")
        if err != nil {
            fmt.Println("无法读取机密信息:", err)
            return
        }

        // 打印读取到的值
        log.Println("读取到的值:", data.Data)
        time.Sleep(time.Second)
    }
</code></pre>
<p>我们运行这个程序将得到如下结果：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:24:17 读取到的值: map[foo:bar]
2023/11/06 05:24:18 读取到的值: map[foo:bar]
2023/11/06 05:24:19 读取到的值: map[foo:bar]
2023/11/06 05:24:20 读取到的值: map[foo:bar]
2023/11/06 05:24:21 读取到的值: map[foo:bar]
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* permission denied
</code></pre>
<p>我们看到如果token过期，而我们的程序又没有<a href="https://developer.hashicorp.com/vault/docs/concepts/lease#lease-durations-and-renewal">对token进行续期(renewal)</a>，程序后续对Vault中机密数据的访问将以”permission denied”的失败而告终。下面我们就来为程序加上token续期，Vault SDK提供了LifetimeWatcher来辅助token续期工作，下面就是利用LifetimeWatcher进行token续期的示例：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    // 设置renewel watcher
    watcher, err := client.NewLifetimeWatcher(&amp;api.LifetimeWatcherInput{
        Secret: secret,
    })
    go watcher.Start()
    defer watcher.Stop()

    kv2 := client.KVv2("secret") // mount "secret"
    ticker := time.NewTicker(time.Second)

    for {
        select {
        case err := &lt;-watcher.DoneCh():
            if err != nil {
                log.Printf("Failed to renew token: %v. Re-attempting login.", err)
                return
            }

            // This occurs once the token has reached max TTL.
            log.Printf("Token can no longer be renewed. Re-attempting login.")
            return

        case renewal := &lt;-watcher.RenewCh():
            // Renewal is now over
            log.Printf("Successfully renewed: %#v", renewal)

        case &lt;-ticker.C:
            // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
            data, err := kv2.Get(context.Background(), "hello")
            if err != nil {
                fmt.Println("无法读取机密信息:", err)
                continue
            }
            // 打印读取到的值
            log.Println("读取到的值:", data.Data)
        }
    }
}

</code></pre>
<p>运行上述示例(此时token_ttl为5s)：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:17:42 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 42, 233750000, time.UTC), Secret:(*api.Secret)(0xc000114a80)}
2023/11/06 05:17:43 读取到的值: map[foo:bar]
2023/11/06 05:17:44 读取到的值: map[foo:bar]
2023/11/06 05:17:45 读取到的值: map[foo:bar]
2023/11/06 05:17:45 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 45, 841374000, time.UTC), Secret:(*api.Secret)(0xc0002827e0)}
2023/11/06 05:17:46 读取到的值: map[foo:bar]
2023/11/06 05:17:47 读取到的值: map[foo:bar]
2023/11/06 05:17:48 读取到的值: map[foo:bar]
2023/11/06 05:17:49 读取到的值: map[foo:bar]
2023/11/06 05:17:49 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 49, 443211000, time.UTC), Secret:(*api.Secret)(0xc0002831a0)}
2023/11/06 05:17:50 读取到的值: map[foo:bar]
2023/11/06 05:17:51 读取到的值: map[foo:bar]
2023/11/06 05:17:52 读取到的值: map[foo:bar]
2023/11/06 05:17:53 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 53, 46880000, time.UTC), Secret:(*api.Secret)(0xc000115a40)}
2023/11/06 05:17:53 读取到的值: map[foo:bar]
2023/11/06 05:17:54 读取到的值: map[foo:bar]
... ...
</code></pre>
<p>我们看到，在token过期之前，LifetimeWatcher帮助Client完成了续期请求。LifetimeWatcher运行在一个单独的goroutine中，通过channel与main goroutine通信。Vault默认token_max_ttl的值为32天，即便你没有设置其值，当token续期到32天时，就无法再renew了，此时watcher.DoneCh会返回事件，这是让你重新login的信号，示例中只给出了注释，并未重新login，大家注意一下。出于安全考虑，可以将token_max_ttl设置为一个合理的值，使其起到应有的安全作用。</p>
<p>通过这个示例我们看到，只要通过Vault的身份认证和授权，我们就能安全地存储和使用机密信息了。那么如何保证应用在与Vault进行身份认证和授权时所使用的凭据的安全呢？比如上面程序里所需的user和password。这个感觉又回到“先有鸡还是先有蛋”的问题了！实际在生产环境，我们可以依赖IaaS层或公有云的安全措施来保证，比如通过环境变量在运行时注入user和password；再比如利用公有云提供的KMS(key management system)或HSM(Hardware Security Module)服务来保证user和password安全。</p>
<h3>4.2  静态secret</h3>
<p>将静态secret作为机密信息保存和管理，是Vault非常常见的应用。secret可以存在很长时间不变，或可能很少改变。Vault可以使用它的加密屏障(barrier)存储这些secret，应用程序运行时可以向Vault请求读取这些secret来使用。</p>
<p>Vault的versioned secrets engine支持你以安全的方式存储和管理secret，同时还提供secret的版本控制能力。你可以使用不同版本的secret进行应用程序升级或回滚，也可以在需要时轻松地恢复旧版本secret。引擎还可以记录secret每个版本的修改人和修改时间。</p>
<p>关于静态secret的管理和使用，可以参见3.5中的基本场景，这里就不赘述了。</p>
<h3>4.3 动态secret</h3>
<p>有静态、长有效期的静态secret，就会有对应的动态secret。和静态secret相比，动态secret安全性高，每个动态secret的有效期都较短，并且一旦泄露可以马上撤销，同时动态secret也便于轮换，定期自动过期无需中断业务。</p>
<p>Vault提供了对多种针对不同系统的动态secret管理能力，包括数据库访问凭据、Active Directory账号, SSH keys和PKI certificates ，Vault针对不同系统提供了不同的secret engine。</p>
<p>Vault官方举了一个有关<a href="https://developer.hashicorp.com/vault/tutorials/db-credentials/database-secrets">使用Database Secrets Engine实现数据库动态secret</a>的示例，</p>
<p>鉴于篇幅，这里也不细说了。</p>
<h3>4.4 其他场景</h3>
<p>根据<a href="https://developer.hashicorp.com/vault/docs/use-cases">Vault官方文档对Vault应用场景的描述</a>，除了静态和动态secret类机密信息，Vault可以处理以下类型的机密信息：</p>
<ul>
<li>数据加密类(Data encryption)机密信息 </li>
</ul>
<p>Vault支持将数据加密服务外包给Vault，应用只需关注数据的加密与解密，Vault负责核心密钥和加密管理。Vault还支持对数据进行传输加密与存储加密。</p>
<ul>
<li>身份识别类(Identity-Based access)机密信息</li>
</ul>
<p>Vault支持从不同身份验证系统整合用户身份，实现统一的ACL系统，管理对系统和应用的访问。</p>
<ul>
<li>加密密钥类(Key management)机密信息</li>
</ul>
<p>Vault支持对云提供商密钥的生命周期管理，例如管理AWS KMS或GCP云密钥。</p>
<p>鉴于篇幅和实验环境有限，这里就针对每种情况做详细示例说明了，大家可以根据自己的需求，针对具体的某个场景做专题性的研究。</p>
<h2>5. 小结</h2>
<p>本文首先介绍了机密管理的概念，阐述了在现代Web应用开发中，为何需要重视机密管理。</p>
<p>接着，文中概述了专用于实现机密管理的机密管理系统的发展历程，以及从功能上逐步演化出的云原生机密管理系统的特征。</p>
<p>文章以业内知名的开源机密管理系统HashiCorp Vault为例，全面系统地介绍了它的架构设计、安全模型、使用方法，并详细阐释了应用程序如何通过与Vault API/SDK的集成，实现对各类机密信息的安全存储、动态生成、访问控制、审计等功能。</p>
<p>最后，文章用代码实例详细演示了基于Vault的几个典型机密管理场景，如不同类型机密信息的读写操作，以及不同认证方式的集成等。</p>
<p>这是个”每个人都应该重视安全的时代”，安全需要每个环节的参与，一处薄弱，就会导致“处处薄弱”。我相信本文的内容能有助于让大家对机密管理的概念、重要性及具体实现方法有更深入的理解。</p>
<p>本文涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/secret-management-examples">这里</a>下载。</p>
<blockquote>
<p>注：Vault项目还提供了Vault Agent和Vault Proxy，旨在为应用提供更可扩展、更简单的方式来集成Vault，消除应用程序采用Vault的初期障碍。Vault Agent可以获取secrets并将它们提供给应用程序，Vault Proxy可以在Vault和应用程序之间充当代理，可选地简化认证过程并缓存请求。有兴趣的童鞋可以参考<a href="https://developer.hashicorp.com/vault/docs/agent-and-proxy">Vault Agent和Proxy的官方文档</a>。</p>
</blockquote>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://arxiv.org/abs/2109.09905">Comparative Analysis of Cryptographic Key Management Systems</a> &#8211; https://arxiv.org/abs/2109.09905</li>
<li><a href="https://arxiv.org/abs/2208.11280">What are the Practices for Secret Management in Software Artifacts?</a> &#8211; https://arxiv.org/abs/2208.11280</li>
<li><a href="https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing">Shamir&#8217;s secret sharing</a> &#8211; https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing</li>
<li><a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp Vault</a> &#8211; https://www.hashicorp.com/blog/vault-announcement</li>
<li><a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Vault Architecture</a> &#8211; https://developer.hashicorp.com/vault/docs/internals/architecture</li>
<li><a href="https://github.com/external-secrets/external-secrets">External Secrets</a> &#8211; https://github.com/external-secrets/external-secrets</li>
<li><a href="https://www.hashicorp.com/resources/5-best-practices-for-secrets-management">5 best practices for secrets management</a> &#8211; https://www.hashicorp.com/resources/5-best-practices-for-secrets-management</li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html">Secrets Management Cheat Sheet</a> &#8211; https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html</li>
<li><a href="https://www.imperva.com/learn/data-security/secret-management/">Secret Management</a> &#8211; https://www.imperva.com/learn/data-security/secret-management/</li>
<li><a href="https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices">Best practices for secrets management in Key Vault</a> &#8211; https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices</li>
<li><a href="https://www.beyondtrust.com/resources/glossary/secrets-management">Glossary: Secrets Management</a> &#8211; https://www.beyondtrust.com/resources/glossary/secrets-management</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>小厂内部私有Go module拉取方案（续）</title>
		<link>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/</link>
		<comments>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/#comments</comments>
		<pubDate>Sat, 18 Jun 2022 14:10:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[access.log]]></category>
		<category><![CDATA[Code-Review]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.14]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodtidy]]></category>
		<category><![CDATA[GOPRIVATE]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[GOSECURE]]></category>
		<category><![CDATA[govanityurls]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[netrc]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[代码评审]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3593</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2 自从去年在公司搭建了内部私有Go module proxy后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。 日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。 突然有一天，业务线考虑将目前在用的gerrit换成gitlab。最初使用gerrit的原因不得而知，但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快，每个迭代的功能都很多，“+2”的review机制到后来就形同虚设了。 如果不用gerrit review工作流，那么gerrit还有什么存在的价值呢。从管理员那边反馈，gerrit配置起来也是比较复杂的，尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是如何让我们内部私有go module代理适配gitlab。 如果你还不清楚我们搭建私有Go module代理的原理，那么在进一步往下阅读前，请先阅读一下《小厂内部私有Go module拉取方案》。 适配gitlab 回顾一下我们的私有Go module代理的原理图： 基于这张原理图，我们分析后得出结论：要适配gitlab仓库，其实很简单，只需修改govanityurls的配置文件中的各个module的真实repo地址即可，这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。 下面我们在gitlab上创建一个foo repo，其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo，先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址：git@10.10.30.30:go/foo.git填到vanity.yaml文件中： //vanity.yaml ... ... /go/foo: repo: ssh://git@10.10.30.30:go/foo.git vcs: git 我门在一台开发机上建立测试程序，该程序导入mycompany.com/go/foo，执行go mod tidy命令的结果如下： $go mod tidy go: finding module for package mycompany.com/go/foo demo imports mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-part2-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2">本文永久链接</a> &#8211; https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2</p>
<p>自从去年在公司<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">搭建了内部私有Go module proxy</a>后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。</p>
<p>日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。</p>
<p>突然有一天，业务线考虑将目前在用的<a href="https://www.gerritcodereview.com">gerrit</a>换成<a href="https://about.gitlab.com">gitlab</a>。最初使用gerrit的原因不得而知，但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快，每个迭代的功能都很多，“+2”的review机制到后来就形同虚设了。</p>
<p>如果不用gerrit review工作流，那么gerrit还有什么存在的价值呢。从管理员那边反馈，gerrit配置起来也是比较复杂的，尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是<strong>如何让我们内部私有go module代理适配gitlab</strong>。</p>
<blockquote>
<p>如果你还不清楚我们搭建私有Go module代理的原理，那么在进一步往下阅读前，请先阅读一下<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">《小厂内部私有Go module拉取方案》</a>。</p>
</blockquote>
<h3>适配gitlab</h3>
<p>回顾一下我们的私有Go module代理的原理图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-9.png" alt="" /></p>
<p>基于这张原理图，我们分析后得出结论：要适配gitlab仓库，其实很简单，只需修改govanityurls的配置文件中的各个module的真实repo地址即可，这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。</p>
<p>下面我们在gitlab上创建一个foo repo，其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo，先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址：git@10.10.30.30:go/foo.git填到vanity.yaml文件中：</p>
<pre><code>//vanity.yaml
  ... ...
  /go/foo:
     repo: ssh://git@10.10.30.30:go/foo.git
     vcs: git
</code></pre>
<p>我门在一台开发机上建立测试程序，该程序导入mycompany.com/go/foo，执行go mod tidy命令的结果如下：</p>
<pre><code>$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: mycompany.com/go/foo@latest: unrecognized import path "mycompany.com/go/foo": http://mycompany.com/go/foo?go-get=1: invalid repo root "ssh://git@10.10.30.30:go/foo.git": parse "ssh://git@10.10.30.30:go/foo.git": invalid port ":go" after host
</code></pre>
<p>从goproxy返回的response内容来看，似乎是goproxy使用的go命令无法识别：”ssh://git@10.10.30.30:go/foo.git”，认为10.10.30.30后面的分号后面应该接一个端口，而不是go。</p>
<p>我们将repo换成下面这样的格式：</p>
<pre><code>  /go/foo:
     repo: ssh://git@10.10.30.30:80/go/foo.git
     vcs: git
</code></pre>
<p>重启govanityurls并重新执行go mod tidy，依旧报错：</p>
<pre><code>$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: module mycompany.com/go/foo: git ls-remote -q origin in /root/.bin/goproxycache/pkg/mod/cache/vcs/4d37c02c151342112bd2d7e6cf9c0508b31b8fe1cf27063da6774aa0f53d872f: exit status 128:
        kex_exchange_identification: Connection closed by remote host
        fatal: Could not read from remote repository.
</code></pre>
<p>直接在主机上通过git clone git@10.10.30.30:80/go/foo.git也是报错的！ssh不行，我们再来试试http方式。 使用http方式呢，每次clone都需要输入用户名密码，不适合goproxy。是时候让personal token上阵了！在gitlab上分配好personal token，然后在本地建立~/.netrc如下：</p>
<pre><code># cat ~/.netrc
machine 10.10.30.30
login tonybai
password [your personal token]
</code></pre>
<p>然后我们将vanity.yaml中的repo改为如下形式：</p>
<pre><code>// vanity.yaml

  /go/foo:
     repo: http://10.10.30.30/go/foo.git
     vcs: git
</code></pre>
<p>这样再执行go mod tidy，foo仓库就被顺利拉取了下来。</p>
<h3>答疑</h3>
<h4>1. git clone错误</h4>
<p>在搭建goproxy时，我们通常会在goproxy服务器上手工验证一下是否可以通过git成功拉取私有仓库，如果git clone出现下面错误信息，是什么问题呢？</p>
<pre><code>$ git clone ssh://tonybai@10.10.30.30:29418/go/common
Cloning into 'common'...
Unable to negotiate with 10.10.30.30 port 29418: no matching key exchange method found. Their offer: diffie-hellman-group14-sha1,diffie-hellman-group1-sha1
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
</code></pre>
<p>这里的错误提示信息其实是很清楚明了的。git服务器端支持diffie-hellman-group1-sha1和diffie-hellman-group14-sha1这两种密钥交换方法，而git客户端却默认一个都不支持。</p>
<p>怎么解决呢？我们需要在goproxy所在主机增加一个配置.ssh/config：</p>
<pre><code>// ~/.ssh/config
Host 10.10.30.30
    HostName 10.10.30.30
    User tonybai
    Port 29418
    KexAlgorithms +diffie-hellman-group1-sha1

    IdentityFile ~/.ssh/id_rsa
</code></pre>
<p>有了这条配置后，我们就可以成功clone。</p>
<h4>2. 使用非安全连接</h4>
<p>有些童鞋使用这个方案后会遇到下面问题：</p>
<pre><code>$go get mycompany.com/go/common@latest
go: module mycompany.com/go/common: reading http://10.10.30.30:10000/mycompany.com/go/common/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/common@latest:
    go list -m: mycompany.com/go/common@latest: unrecognized import path "mycompany.com/go/common": https fetch: Get "https://mycompany.com/go/common?go-get=1": dial tcp 127.0.0.1:443: connect: connection refused
</code></pre>
<p>首先，go get得到的服务端响应信息中提示：无法连接127.0.0.1:443，查看goproxy主机的nginx access.log，也无日志。说明goproxy没有发起请求。也就是说问题出在go list命令这块，它为什么要去连127.0.0.1:443？我们的代码服务器使用的可是http而非https方式访问。</p>
<p>这让我想起了Go 1.14中增加的GOINSECURE，go命令默认采用的是secure方式，即https去访问代码仓库的。如果不要求非得以https获取module，或者即便使用https，也不再对server证书进行校验，那么需要设置GOINSECURE环境变量，比如；</p>
<pre><code>export GOINSECURE="mycompany.com"
</code></pre>
<p>这样再获取mycompany.com/&#8230;下面的go module时，就不会出现上面的错误了！</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Hello，WireGuard</title>
		<link>https://tonybai.com/2020/03/29/hello-wireguard/</link>
		<comments>https://tonybai.com/2020/03/29/hello-wireguard/#comments</comments>
		<pubDate>Sun, 29 Mar 2020 08:29:01 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apt-get]]></category>
		<category><![CDATA[BLAKE2]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[curve25519]]></category>
		<category><![CDATA[DKMS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ethr]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[ipsec]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[ip_forward]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[l2tp]]></category>
		<category><![CDATA[Linus]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[netfilter]]></category>
		<category><![CDATA[netns]]></category>
		<category><![CDATA[noise-framework]]></category>
		<category><![CDATA[openvpn]]></category>
		<category><![CDATA[overlay]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[Poly1305]]></category>
		<category><![CDATA[pptp]]></category>
		<category><![CDATA[SipHash24]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sysctl]]></category>
		<category><![CDATA[tailscale]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[virtual-private-network]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[wg]]></category>
		<category><![CDATA[wg-quick]]></category>
		<category><![CDATA[wg0]]></category>
		<category><![CDATA[WireGuard]]></category>
		<category><![CDATA[wireguard-go]]></category>
		<category><![CDATA[wormhole]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[动态内核模块技术]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[带宽]]></category>
		<category><![CDATA[控制平面]]></category>
		<category><![CDATA[数据平面]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[隧道]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2879</guid>
		<description><![CDATA[2020年1月28日，Linux之父Linus Torvalds正式将WireGuard merge到Linux 5.6版本内核主线： 图：WireGuard被加入linux kernel 5.6主线的commit log 这意味着在Linux 5.6内核发布时，linux在内核层面将原生支持一个新的VPN协议栈：WireGuard。 图：WireGuard Logo 一. VPN与WireGuard的创新 VPN，全称Virtual Private Network（虚拟专用网络）。提起VPN，大陆的朋友想到的第一件事就是fan qiang。其实fan qiang只是VPN的一个“小众”应用罢了^_^，企业网络才是VPN真正施展才能的地方。VPN支持在不安全的公网上建立一条加密的、安全的到企业内部网络的通道（隧道tunnel），这就好比专门架设了一个专用网络那样。在WireGuard出现之前，VPN的隧道协议主要有PPTP、L2TP和IPSec等，其中PPTP和L2TP协议工作在OSI模型的第二层，又称为二层隧道协议；IPSec是第三层隧道协议。 既然已经有了这么多的VPN协议，那么Why WireGuard？ WireGuard的作者Jason A. Donenfeld在WireGuard官网给出了很明确地理由： 简单、易用、无连接、无状态：号称目前最易用和最简单的VPN解决方案 WireGuard可以像SSH一样易于配置和部署。只需交换非常简单的公钥就可以建立VPN连接，就像交换SSH密钥一样，其余所有由WireGuard透明处理。并且WireGuard建立的VPN连接是基于UDP的，无需建立和管理连接，无需关心和管理状态的。 先进加密协议 WireGuard充分利用安全领域和密码学在这些年的最新成果，使用noise framework，Curve25519，ChaCha20，Poly1305，BLAKE2，SipHash24等构建WireGuard的安全方案。 最小的攻击面(最少代码实现) WireGuard的内核模块c代码仅不足5k行，便于代码安全评审。也使得WireGuard的实现更不容易被攻击（代码量少，理论上漏洞相对于庞大的代码集合而言也会少许多）。 高性能 密码学最新成果带来的高速机密原语和WireGuard的内核驻留机制，使其相较于之前的VPN方案更具性能优势。 以上这些理由，同时也是WireGuard这个协议栈的特性。 这么说依然很抽象，我们来实操一下，体验一下WireGuard的简洁、易用、安全、高效。 二. WireGuard安装和使用 WireGuard将在linux 5.6内核中提供原生支持，也就是说在那之前，我们还无法直接使用WireGuard，安装还是不可避免的。在我的实验环境中有两台Linux VPS主机，都是ubuntu 18.04，内核都是4.15.0。因此我们需要首先添加WireGuard的ppa仓库： sudo add-apt-repository ppa:wireguard/wireguard 更新源后，即可通过下面命令安装WireGuard： sudo apt-get update sudo apt-get install wireguard 安装的WireGuard分为两部分： WireGuard内核模块(wireguard.ko)，这部分通过动态内核模块技术DKMS安装到ubuntu的内核模块文件目录下： $ [...]]]></description>
			<content:encoded><![CDATA[<p>2020年1月28日，Linux之父<a href="https://github.com/torvalds">Linus Torvalds</a>正式将<a href="https://www.wireguard.com/">WireGuard</a> merge<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bd2463ac7d7ec51d432f23bf0e893fb371a908cd">到Linux 5.6版本内核主线</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-add-to-linux-kernel-5.6-next.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard被加入linux kernel 5.6主线的commit log</center></p>
<p>这意味着在Linux 5.6内核发布时，linux在内核层面将<strong>原生</strong>支持一个新的VPN协议栈：<a href="https://git.zx2c4.com/wireguard">WireGuard</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-wireguard-logo.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard Logo</center></p>
<h2>一. VPN与WireGuard的创新</h2>
<p>VPN，全称Virtual Private Network（虚拟专用网络）。提起VPN，大陆的朋友想到的第一件事就是fan qiang。其实fan qiang只是VPN的一个“小众”应用罢了^_^，企业网络才是VPN真正施展才能的地方。VPN支持在不安全的公网上建立一条加密的、安全的到企业内部网络的通道（隧道tunnel），这就好比专门架设了一个专用网络那样。在WireGuard出现之前，VPN的隧道协议主要有<a href="https://tools.ietf.org/html/rfc2637">PPTP</a>、<a href="https://tools.ietf.org/html/rfc2661">L2TP</a>和<a href="https://tools.ietf.org/html/rfc4301">IPSec</a>等，其中PPTP和L2TP协议工作在OSI模型的第二层，又称为二层隧道协议；IPSec是第三层隧道协议。</p>
<p>既然已经有了这么多的VPN协议，那么<strong>Why WireGuard？</strong></p>
<p>WireGuard的作者<a href="https://github.com/zx2c4">Jason A. Donenfeld</a>在<a href="https://www.wireguard.com/">WireGuard官网</a>给出了很明确地理由：</p>
<ul>
<li>简单、易用、无连接、无状态：号称目前最易用和最简单的VPN解决方案</li>
</ul>
<p>WireGuard可以像SSH一样易于配置和部署。只需交换非常简单的公钥就可以建立VPN连接，就像交换SSH密钥一样，其余所有由WireGuard透明处理。并且WireGuard建立的VPN连接是基于UDP的，无需建立和管理连接，无需关心和管理状态的。</p>
<ul>
<li>先进加密协议</li>
</ul>
<p>WireGuard充分利用安全领域和密码学在这些年的最新成果，使用<a href="http://www.noiseprotocol.org/">noise framework</a>，<a href="http://cr.yp.to/ecdh.html">Curve25519</a>，<a href="http://cr.yp.to/chacha.html">ChaCha20</a>，<a href="http://cr.yp.to/mac.html">Poly1305</a>，<a href="https://blake2.net/">BLAKE2</a>，<a href="https://131002.net/siphash/">SipHash24</a>等构建WireGuard的安全方案。</p>
<ul>
<li>最小的攻击面(最少代码实现)</li>
</ul>
<p>WireGuard的内核模块c代码仅不足5k行，便于代码安全评审。也使得WireGuard的实现更不容易被攻击（代码量少，理论上漏洞相对于庞大的代码集合而言也会少许多）。</p>
<ul>
<li>高性能</li>
</ul>
<p>密码学最新成果带来的高速机密原语和WireGuard的内核驻留机制，使其相较于之前的VPN方案更具性能优势。</p>
<p>以上这些理由，同时也是WireGuard这个协议栈的特性。</p>
<p>这么说依然很抽象，我们来实操一下，体验一下WireGuard的简洁、易用、安全、高效。</p>
<h2>二. WireGuard安装和使用</h2>
<p>WireGuard将在linux 5.6内核中提供原生支持，也就是说在那之前，我们还无法直接使用WireGuard，安装还是不可避免的。在我的实验环境中有两台Linux VPS主机，都是<a href="https://tonybai.com/tag/ubuntu">ubuntu 18.04</a>，内核都是4.15.0。因此我们需要首先添加WireGuard的ppa仓库：</p>
<pre><code>sudo add-apt-repository ppa:wireguard/wireguard

</code></pre>
<p>更新源后，即可通过下面命令安装WireGuard：</p>
<pre><code>sudo apt-get update

sudo apt-get install wireguard

</code></pre>
<p>安装的WireGuard分为两部分：</p>
<ul>
<li>WireGuard内核模块(wireguard.ko)，这部分通过动态内核模块技术<a href="https://baike.baidu.com/item/DKMS/9743354">DKMS</a>安装到ubuntu的内核模块文件目录下：</li>
</ul>
<pre><code>$ ls /lib/modules/4.15.0-29-generic/updates/dkms/
wireguard.ko

</code></pre>
<ul>
<li>用户层的命令行工具</li>
</ul>
<p>类似于内核netfilter和命令行工具iptables之间关系，wireguard.ko对应的用户层命令行工具wireguard-tools：<code>wg、wg-quick</code>被安装到/usr/bin下面了：</p>
<pre><code>$ ls -t /usr/bin|grep wg|head -n 2
wg
wg-quick

</code></pre>
<h3>1. peer to peer vpn</h3>
<p>在两个linux Vps上都安装完WireGuard后，我们就可以在两个节点(peer)建立虚拟专用网络(VPN)了。我们分为称两个linux节点为peer1和peer2：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-peer-to-peer-1.png" alt="img{512x368}" /></p>
<p><center>图：点对点wireguard通信图</center></p>
<p>就像上图那样，我们只分别需要在peer1和peer2建立<code>/etc/wireguard/wg0.conf</code>。</p>
<p>peer1的<code>/etc/wireguard/wg0.conf</code>：</p>
<pre><code>[Interface]
PrivateKey = {peer1's privatekey}
Address = 10.0.0.1
ListenPort = 51820

[Peer]
PublicKey = {peer2's publickey}
EndPoint = {peer2's ip}:51820
AllowedIPs = 10.0.0.2/32

</code></pre>
<p>peer2的<code>/etc/wireguard/wg0.conf</code>：</p>
<pre><code>[Interface]
PrivateKey = {peer2's privatekey}
Address = 10.0.0.2
ListenPort = 51820

[Peer]
PublicKey = {peer1's publickey}
EndPoint = {peer1's ip}:51820
AllowedIPs = 10.0.0.1/32

</code></pre>
<p>我们看到每个peer上WireGuard所需的配置文件wg0.conf包含两大部分：</p>
<ul>
<li>
<p><code>[Interface]部分</code></p>
<ul>
<li>
<p>PrivateKey &#8211;  peer自身的privatekey</p>
</li>
<li>
<p>Address &#8211; peer的wg0接口在vpn网络中绑定的路由ip范围，在上述例子中仅绑定了一个ip地址</p>
</li>
<li>
<p>ListenPort &#8211; wg网络协议栈监听UDP端口</p>
</li>
</ul>
</li>
<li>
<p><code>[Peer]部分</code>（描述vpn网中其他peer信息，一个wg0配置文件中显然可以配置多个Peer部分）</p>
<ul>
<li>
<p>PublicKey &#8211; 该peer的publickey</p>
</li>
<li>
<p>EndPoint &#8211; 该peer的wg网路协议栈地址(ip+port)</p>
</li>
<li>
<p>AllowedIPs &#8211; 允许该peer发送过来的wireguard载荷中的源地址范围。同时本机而言，这个字段也会作为本机路由表中wg0绑定的ip范围。</p>
</li>
</ul>
</li>
</ul>
<p>每个Peer自身的privatekey和publickey可以通过WireGuard提供的命令行工具生成：</p>
<pre><code>$ wg genkey | tee privatekey | wg pubkey &gt; publickey
$ ls
privatekey  publickey
</code></pre>
<blockquote>
<p>注：这两个文件可以生成在任意路径下，我们要的是两个文件中内容。</p>
</blockquote>
<p>在两个peer上配置完<code>/etc/wireguard/wg0.conf</code>配置文件后，我们就可以使用下面命令<strong>在peer1和peer2之间建立一条双向加密VPN隧道</strong>了：</p>
<pre><code>peer1:

$ sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.1 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.0.0.2/32 dev wg0

peer2:

$ sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.2 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.0.0.1/32 dev wg0

</code></pre>
<p>执行上述命令，每个peer会增加一个network interface dev: <strong>wg0</strong>，并在系统路由表中增加一条路由，以peer1为例：</p>
<pre><code>$ ip a

... ...

4: wg0: &lt;POINTOPOINT,NOARP,UP,LOWER_UP&gt; mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.0.1/32 scope global wg0
       valid_lft forever preferred_lft forever

$ ip route
default via 172.21.0.1 dev eth0 proto dhcp metric 100
10.0.0.2 dev wg0 scope link
... ...

</code></pre>
<p>现在我们来测试两个Peer之间的连通性。<strong>WireGuard的peer之间是对等的</strong>，谁发起的请求谁就是client端。我们在peer1上ping peer2，在peer2上我们用tcpdump抓wg0设备的包：</p>
<pre><code>Peer1:

$ ping -c 3 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=34.9 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=34.7 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=34.6 ms

--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 34.621/34.781/34.982/0.262 ms

Peer2:

# tcpdump -i wg0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
13:29:52.659550 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 1, length 64
13:29:52.659603 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 1, length 64
13:29:53.660463 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 2, length 64
13:29:53.660495 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 2, length 64
13:29:54.662201 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 3, length 64
13:29:54.662234 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 3, length 64

</code></pre>
<p>我们看到peer1和peer2经由WireGuard建立的vpn实现了连通：在peer2上ping peer1(10.0.0.1)亦得到相同结果。</p>
<p>这时如果我们如果在peer2(vpn ip: 10.0.0.2)上启动一个http server(监听0.0.0.0:9090):</p>
<pre><code>//httpserver.go
package main

import "net/http"

func index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello, wireguard\n"))
}

func main() {
    http.Handle("/", http.HandlerFunc(index))
    http.ListenAndServe(":9090", nil)
}
</code></pre>
<p>那么我们在peer1(vpn ip:10.0.0.1)去访问这个server：</p>
<pre><code>$ curl http://10.0.0.2:9090
hello, wireguard

</code></pre>
<p>在peer2(instance-cspzrq3u)上的tcpdump显示(tcp握手+数据通信+tcp拆除)：</p>
<pre><code>14:15:05.233794 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [S], seq 1116349511, win 27600, options [mss 1380,sackOK,TS val 3539789774 ecr 0,nop,wscale 7], length 0
14:15:05.233854 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [S.], seq 3504538202, ack 1116349512, win 27360, options [mss 1380,sackOK,TS val 2842719516 ecr 3539789774,nop,wscale 7], length 0
14:15:05.268792 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 0
14:15:05.268882 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [P.], seq 1:78, ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 77
14:15:05.268907 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [.], ack 78, win 214, options [nop,nop,TS val 2842719551 ecr 3539789809], length 0
14:15:05.269514 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [P.], seq 1:134, ack 78, win 214, options [nop,nop,TS val 2842719552 ecr 3539789809], length 133
14:15:05.304147 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0
14:15:05.304194 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [F.], seq 78, ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0
14:15:05.304317 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [F.], seq 134, ack 79, win 214, options [nop,nop,TS val 2842719586 ecr 3539789845], length 0
14:15:05.339035 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 3539789880 ecr 2842719586], length 0

</code></pre>
<p>如果要拆除这个vpn，只需在每个peer上分别执行如下命令：</p>
<pre><code>$ sudo wg-quick down wg0
[#] ip link delete dev wg0

</code></pre>
<h3>2. peer to the local network of other peer</h3>
<p>上面两个peer虽然实现了点对点的连通，但是如果我们想从peer1访问peer2所在的局域网中的另外一台机器（这显然是vpn最常用的应用场景），如下面示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-peer-to-the-network-of-other-peer-1.png" alt="img{512x368}" /></p>
<p><center>图：从一个peer到另外一个peer所在局域网的节点的通信图</center></p>
<p>基于目前的配置是否能实现呢？我们来试试。首先我们在peer1上要将<code>192.168.1.0/24</code>网段的路由指到wg0上，这样我们在peer1上ping或curl 192.168.1.123:9090，数据才能被交给wg0处理并通过vpn网络送出，修改peer1上的wg0.conf：</p>
<pre><code>// peer1's /etc/wireguard/wg0.conf

... ...
[Peer]
PublicKey = {peer2's publickey}
EndPoint = peer2's ip:51820
AllowedIPs = 10.0.0.2/32,192.168.1.0/24

</code></pre>
<p>重启peer1上的wg0使上述配置生效。然后我们尝试在peer1上ping 192.168.1.123：</p>
<pre><code>$ ping -c 3 192.168.1.123
PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data.

--- 192.168.1.123 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2038ms

</code></pre>
<p>我们在peer2上的tcpdump显示：</p>
<pre><code># tcpdump -i wg0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
14:33:38.393520 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 1, length 64
14:33:39.408083 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 2, length 64
14:33:40.432079 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 3, length 64

</code></pre>
<p>我们看到peer2收到来自10.0.0.1的到192.168.1.123的ping包都<strong>没有对应的回包</strong>，通信失败。Why？我们分析一下。</p>
<p>peer2在51820端口收到WireGuard包后，去除wireguard包的包裹，露出真实数据包。真实数据包的目的ip地址为192.168.1.123，该地址并非peer2自身地址(其自身局域网地址为192.168.1.10)。既然不是自身地址，就不能送到上层协议栈(tcp)处理，那么另外一条路是forward(转发)出去。但是是否允许转发么？显然从结果来看，从wg0收到的消息无权转发，于是消息丢弃，这就是没有回包和通信失败的原因。</p>
<p>为了支持转发（这是vpn常用场景的功能哦），我们需要为peer2的wg0.conf增加些转发配置：</p>
<pre><code>// peer2's  wg0.conf

[Interface]

... ...
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUT  ING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUT  ING -o eth0 -j MASQUERADE

... ...

</code></pre>
<p>重启peer2的wg0。在peer2的内核层我们也要开启转发开关：</p>
<pre><code>// /etc/sysctl.conf

net.ipv4.ip_forward=1

net.ipv6.conf.all.forwarding=1

</code></pre>
<p>执行下面命令临时生效：</p>
<pre><code># sysctl -p
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

</code></pre>
<p>接下来，我们再来测试一下连通性。我们在peer1上再次尝试<code>ping 192.168.1.123</code>：</p>
<pre><code>$ ping -c 3 192.168.1.123
PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data.
64 bytes from 192.168.1.123: icmp_seq=1 ttl=46 time=200 ms
64 bytes from 192.168.1.123: icmp_seq=2 ttl=46 time=200 ms
64 bytes from 192.168.1.123: icmp_seq=3 ttl=46 time=200 ms

--- 192.168.1.123 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 200.095/200.239/200.396/0.531 ms

</code></pre>
<p>这回通了！peer2上的Tcpdump输出中也看到了回包：</p>
<pre><code>14:49:58.808467 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 1, length 64
14:49:58.974035 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 1, length 64
14:49:59.809747 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 2, length 64
14:49:59.975240 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 2, length 64
14:50:00.810802 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 3, length 64
14:50:00.976202 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 3, length 64
</code></pre>
<p>我们在192.168.1.123上运行上面的那个httpserver程序，再在peer1上用curl访问这个程序：</p>
<pre><code>$ curl 192.168.1.123:9090
hello, wireguard
</code></pre>
<p>我们看到httpserver的应答成功返回。peer2上的tcpdump也抓到了整个通信过程：</p>
<pre><code>14:50:36.437259 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [S], seq 3235649864, win 27600, options [mss 1380,sackOK,TS val 101915019 ecr 0,nop,wscale 7], length 0
14:50:36.593554 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [S.], seq 2420552016, ack 3235649865, win 28960, options [mss 1460,sackOK,TS val 2323314775 ecr 101915019,nop,wscale 7], length 0
14:50:36.628315 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 0
14:50:36.628379 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [P.], seq 1:84, ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 83
14:50:36.784550 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [.], ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 0
14:50:36.784710 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [P.], seq 1:134, ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 133
14:50:36.820339 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0
14:50:36.820383 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [F.], seq 84, ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0
14:50:36.977226 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [F.], seq 134, ack 85, win 227, options [nop,nop,TS val 2323314870 ecr 101915401], length 0
14:50:37.011927 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 101915594 ecr 2323314870], length 0

</code></pre>
<h3>3. WireGuard的用户层实现</h3>
<p>在linux上，我们务必使用WireGuard的内核模式，这显然是最高效的。在macOS、Windows上，WireGuard无法以内核模块驻留模式运行，但WireGuard项目提供了WireGuard的用户层实现。其作者<a href="https://github.com/zx2c4">Jason A. Donenfeld</a>亲自实现了<a href="https://tonybai.com/tag/go">Go语言</a>版本的<a href="https://git.zx2c4.com/wireguard-go">wireguard-go</a>。macOS上使用的就是wireguard的Go实现。我们可以使用brew在macOS上按照WireGuard：</p>
<pre><code>$brew install wireguard-tools

</code></pre>
<p>配置好<code>/etc/wireguard/wg0.conf</code>后(和linux上的配置方式一致)，同样可以通过wg-quick命令启动wireguard：</p>
<pre><code>$sudo wg-quick up wg0

</code></pre>
<p>wg-quick实际上会通过<code>wireguard-go</code>来实现linux wireguard在内核中完成的功能：</p>
<pre><code>$ps -ef|grep wireguard

    0 57783     1   0  3:18下午 ttys002    0:00.01 wireguard-go utun

</code></pre>
<h2>三. WireGuard性能如何</h2>
<p>关于WireGuard性能如何，官方给出了一个性能基准测试的对比数据（相较于其他vpn网络栈）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-performance-1.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard性能与其他vpn网络栈的对比（来自官方截图）</center></p>
<p>我们看到和IPSec、OpenVPN相比，无论从吞吐还是延迟，WireGuard都领先不少。</p>
<p>我们这里用<a href="https://github.com/Microsoft/ethr">microsoft开源的带宽测试工具ethr</a>来直观看一下走物理网络和走WireGuard VPN的带宽差别。</p>
<p>在peer2上运行：</p>
<pre><code>$ ethr -s

</code></pre>
<p>然后在peer1上分别通过物理网络和VPN网络向peer2发起请求：</p>
<ul>
<li>peer1 -> peer2 (物理网络)</li>
</ul>
<pre><code>$ ethr -c  peer2's ip
Connecting to host [peer2 ip], port 9999
[  6] local 172.21.0.5 port 46108 connected to  peer2 ip port 9999
- - - - - - - - - - - - - - - - - - - - - - -
[ ID]   Protocol    Interval      Bits/s
[  6]     TCP      000-001 sec     1.54M
[  6]     TCP      001-002 sec     1.54M
[  6]     TCP      002-003 sec     1.54M
[  6]     TCP      003-004 sec     1.54M
[  6]     TCP      004-005 sec     1.54M

.... ...

</code></pre>
<ul>
<li>peer1 -> peer2 (vpn网络)</li>
</ul>
<pre><code>$ ethr -c 10.0.0.2
Connecting to host [10.0.0.2], port 9999
[  6] local 10.0.0.1 port 36010 connected to 10.0.0.2 port 9999
- - - - - - - - - - - - - - - - - - - - - - -
[ ID]   Protocol    Interval      Bits/s
[  6]     TCP      000-001 sec     1.79M
[  6]     TCP      001-002 sec      640K
[  6]     TCP      002-003 sec     1.15M
[  6]     TCP      003-004 sec      512K
[  6]     TCP      004-005 sec     1.02M
[  6]     TCP      005-006 sec     1.02M
[  6]     TCP      006-007 sec     1.02M

</code></pre>
<p>我们看到走vpn的带宽相当于走物理网络的<strong>66%</strong>(1.02/1.54)左右。这里peer1(腾讯云)、peer2(百度云)之间走的是互联网，而在局域网测试的效果可能更好（留给大家^_^）。</p>
<h2>四. 小结</h2>
<p>经过上面的实验，我们看到了WireGuard的配置的确十分简单，这也是我目前使用过的配置过程最为简单的vpn。随着linux kernel 5.6内置对WireGuard的原生支持，WireGuard在vpn领域势必会有更为广泛的应用。</p>
<p>在容器网络方面，目前WireGuard已经给出了<a href="https://www.wireguard.com/netns/">跨容器的网络通信方案</a>，基于wireguard的k8s cni网络插件<a href="https://github.com/gravitational/wormhole">wormhole</a>可以让pod之间通过wireguard实现的overlay网络通信。</p>
<p>国外的tailscale公司正在实现<a href="https://tailscale.com/blog/how-tailscale-works/">一种基于Wireguard的mesh vpn网络</a>，该网络以WireGuard为数据平面的承载体，该公司主要实现控制平面。该公司目前聚集了一些Go核心开发人员，这里就包括著名的go核心开发团队成员、net/http包的最初作者和当前维护者的Brad Fitzpatrick。</p>
<h2>五. 参考资料</h2>
<ul>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/91383212">WireGuard，简约之美</a> &#8211; https://zhuanlan.zhihu.com/p/91383212 原理说明，墙裂推荐！</p>
</li>
<li>
<p><a href="https://baike.baidu.com/item/虚拟专用网络/8747869">虚拟专用网络</a> &#8211; https://baike.baidu.com/item/虚拟专用网络/8747869</p>
</li>
<li>
<p><a href="https://www.wireguard.com/">WireGuard官网资料</a> &#8211; https://www.wireguard.com/</p>
</li>
<li>
<p><a href="https://github.com/pirate/wireguard-docs">非官方WireGuard文档</a> &#8211; https://github.com/pirate/wireguard-docs</p>
</li>
<li>
<p><a href="https://www.stavros.io/posts/how-to-configure-wireguard/">How to easily configure WireGuard</a> &#8211; https://www.stavros.io/posts/how-to-configure-wireguard/</p>
</li>
<li>
<p><a href="https://www.ericlight.com/wireguard-part-one-installation.html">WireGuard series</a> &#8211; https://www.ericlight.com/wireguard-part-one-installation.html</p>
</li>
<li>
<p><a href="https://www.lixh.cn/archives/2165.html">MacOS下WireGuard客户端的安装和配置</a></p>
</li>
</ul>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/03/29/hello-wireguard/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Kubernetes网络插件（CNI）基准测试的最新结果</title>
		<link>https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/</link>
		<comments>https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/#comments</comments>
		<pubDate>Thu, 18 Apr 2019 13:32:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[canal]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[configmap]]></category>
		<category><![CDATA[egress]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[iperf3]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kops]]></category>
		<category><![CDATA[kube-router]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[kubespray]]></category>
		<category><![CDATA[MTU]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[rancher]]></category>
		<category><![CDATA[romana]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[weavenet]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[基准测试]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[插件]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2702</guid>
		<description><![CDATA[本文翻译自Alexis Ducastel的文章《Benchmark results of Kubernetes network plugins (CNI) over 10Gbit/s network (Updated: April 2019)》。 本文是我之前的基准测试的最新更新，这次测试在最新版Kubernetes 1.14上运行，其中CNI版本在2019年4月更新。 首先，非常感谢Cilium团队对我的帮助，包括协助审查测试结果以及更正我的指标监控脚本。 自2018年11月以来都有哪些新变化 如果你只是想知道自上次以来发生的变化，这里有一个简短的总结： Flannel仍然是CNI竞赛中最快和最精简的那个选手，但它仍然不支持NetworkPolicies(网络策略)，也不支持加密。 Romana不再维护，因此我们决定将其从基准测试中剔除。 WeaveNet现在同时支持Ingress和Egress的NetworkPolicies！但性能要略低于之前的版本。 如果您想获得最佳性能，Calico仍需要手动定制MTU。Calico为安装CNI提供了两个新选项，无需专用ETCD存储： 将状态存储在Kubernetes API中作为数据存储区（集群&#60;50个节点） 使用Typha代理将状态存储在Kubernetes API中，以减轻K8S API（集群> 50个节点）的压力 Calico宣布在Istio之上支持应用层策略(Application Layer Policy)，为应用层带来安全性。 Cilium现在支持加密！Cilium使用IPSec隧道提供加密，并为WeaveNet提供了加密网络的替代方案。但是，在启用加密的情况下，WeaveNet比Cilium更快。这是由于Cilium 1.4.2仅支持CBC加密，若使用GCM将会更好，但它将是1.5版本的Cilium的一部分。 由于嵌入了ETCD operator，因此Cilium现在更容易部署。 Cilium团队还通过降低内存消耗和CPU成本，努力减少CNI占用空间。但他们仍然比其他选手更重。 基准测试的上下文 基准测试是在通过Supermicro 10Gbit交换机连接的三台Supermicro裸机服务器上进行的。服务器通过DAC SFP +无源电缆直接连接到交换机，并在激活巨型帧（MTU 9000）的同一VLAN中设置。 Kubernetes 1.14.0​在Ubuntu 18.04 LTS上运行，运行Docker 18.09.2（此linux版本中的默认docker版本）。 为了提高可重复性，我们选择始终在第一个节点上设置master，在第二个服务器上设置基准测试的服务器部分，在第三个服务器上设置客户端部分。这是通过Kubernetes deployments中的NodeSelector实现的。 以下是我们将用于描述基准测试结果和解释的表情图： 为基准测试选择CNI 这个基准测试仅仅关注那些入选kubernetes正式文档：“create a single [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自Alexis Ducastel的文章<a href="https://itnext.io/benchmark-results-of-kubernetes-network-plugins-cni-over-10gbit-s-network-updated-april-2019-4a9886efe9c4">《Benchmark results of Kubernetes network plugins (CNI) over 10Gbit/s network (Updated: April 2019)》</a>。</p>
<p>本文是我<a href="https://itnext.io/benchmark-results-of-kubernetes-network-plugins-cni-over-10gbit-s-network-36475925a560">之前的基准测试</a>的最新更新，这次测试在最新版<a href="https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement/">Kubernetes 1.14</a>上运行，其中CNI版本在2019年4月更新。</p>
<p>首先，非常感谢<a href="https://cilium.io/">Cilium团队</a>对我的帮助，包括协助审查测试结果以及更正我的指标监控脚本。</p>
<h2>自2018年11月以来都有哪些新变化</h2>
<p>如果你只是想知道自上次以来发生的变化，这里有一个简短的总结：</p>
<p><a href="https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/">Flannel</a>仍然是<a href="https://github.com/containernetworking/cni">CNI</a>竞赛中最快和最精简的那个选手，但它仍然不支持NetworkPolicies(网络策略)，也不支持加密。</p>
<p><a href="https://github.com/romana/cni">Romana</a>不再维护，因此我们决定将其从基准测试中剔除。</p>
<p><a href="https://github.com/weaveworks/weave">WeaveNet</a>现在同时支持<a href="https://tonybai.com/2018/06/21/kubernetes-ingress-controller-practice-using-four-examples/">Ingress</a>和Egress的NetworkPolicies！但性能要略低于之前的版本。</p>
<p>如果您想获得最佳性能，<a href="https://www.projectcalico.org/">Calico</a>仍需要手动定制MTU。Calico为安装CNI提供了两个新选项，无需专用<a href="https://tonybai.com/tag/etcd">ETCD存储</a>：</p>
<ul>
<li>将状态存储在Kubernetes API中作为数据存储区（集群&lt;50个节点）</li>
<li>使用Typha代理将状态存储在Kubernetes API中，以减轻K8S API（集群> 50个节点）的压力</li>
</ul>
<p>Calico宣布在<a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">Istio</a>之上支持应用层策略(Application Layer Policy)，为应用层带来安全性。</p>
<p>Cilium现在支持加密！Cilium使用IPSec隧道提供加密，并为WeaveNet提供了加密网络的替代方案。但是，在启用加密的情况下，WeaveNet比Cilium更快。这是由于Cilium 1.4.2仅支持CBC加密，若使用GCM将会更好，但它将是1.5版本的Cilium的一部分。</p>
<p>由于嵌入了ETCD operator，因此Cilium现在更容易部署。</p>
<p>Cilium团队还通过降低内存消耗和CPU成本，努力减少CNI占用空间。但他们仍然比其他选手更重。</p>
<h2>基准测试的上下文</h2>
<p>基准测试是在通过Supermicro 10Gbit交换机连接的三台Supermicro裸机服务器上进行的。服务器通过DAC SFP +无源电缆直接连接到交换机，并在激活巨型帧（MTU 9000）的同一VLAN中设置。</p>
<p><a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">Kubernetes</a> 1.14.0​在<a href="https://tonybai.com/tag/ubuntu">Ubuntu</a> 18.04 LTS上运行，运行<a href="https://tonybai.com/tag/docker">Docker</a> 18.09.2（此linux版本中的默认docker版本）。</p>
<p>为了提高可重复性，我们选择始终在第一个节点上设置master，在第二个服务器上设置基准测试的服务器部分，在第三个服务器上设置客户端部分。这是通过Kubernetes deployments中的NodeSelector实现的。</p>
<p>以下是我们将用于描述基准测试结果和解释的表情图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_CejvF2mlwcV27sAy-sz1VQ.png" alt="img{512x368}" /></p>
<h2>为基准测试选择CNI</h2>
<p>这个基准测试仅仅关注那些入选kubernetes正式文档：<a href="https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/">“create a single master cluster with kubeadm”</a>中的CNI列表。在提到的9个CNI中，我们只测试其中的6个，不包括那些我们无法轻松安装和/或不通过以下文档开箱即用的工具（Romana，Contiv-VPP和JuniperContrail / TungstenFabric）</p>
<p>以下是我们将要比较的CNI列表：</p>
<ul>
<li>Calico v3.6</li>
<li>Canal v3.6（事实上，Flannel用于网络+ Calico用于防火墙）</li>
<li>Cilium 1.4.2</li>
<li>Flannel 0.11.0</li>
<li>Kube-router 0.2.5</li>
<li>WeaveNet 2.5.1</li>
</ul>
<h2>安装</h2>
<p>CNI越容易设置，我们对其第一印象就越好。所有参与基准测试的CNI都很容易设置（一个或两个命令行）。</p>
<p>如前所述，服务器和交换机都配置了Jumbo帧激活（通过将MTU设置为9000）。我们非常感谢CNI可以自动发现要使用的MTU，具体取决于适配器。事实上，Cilium和Flannel是唯一能够正确自动检测MTU的选手。大多数其他CNI在GitHub中引发了启用MTU自动检测的问题，但是现在，我们需要通过修改Calico，Canal和Kube-router的ConfigMap或WeaveNet的ENV var来手动修复它。</p>
<p>也许您想知道错误的MTU会产生什么影响？这里有一个图表，显示WeaveNet与默认MTU和WeaveNet与Jumbo帧之间的区别：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_kQcutZL8nDrncSUbFAdcag.png" alt="img{512x368}" /></p>
<p>那么，既然我们知道MTU对性能非常重要，那么这些CNI如何自动检测MTU：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_Nr--EbXRgNeHQzyLdFvinQ.png" alt="img{512x368}" /></p>
<p>正如我们在上图中看到的，我们必须对Calico，Canal，Kube-router和WeaveNet应用一些MTU调整以获得最佳性能。Cilium和Flannel能够自行正确地自动检测MTU，确保开箱即用的最佳性能。</p>
<h2>安全</h2>
<p>在比较这些CNI的安全性时，我们谈论两件事：它们加密通信的能力，以及它们对Kubernetes网络策略的实现（根据实际测试，而不是来自他们的文档）。</p>
<p>只有两个CNI可以实现加密通信：Cilium和WeaveNet。通过将加密密码设置为CNI的ENV变量可以来启用WeaveNet加密。WeaveNet文档有点令人困惑，但这很容易做到。Cilium加密是通过创建Kubernetes Secrets和daemonSet修改的命令设置的（比WeaveNet复杂一点，但是Cilium有很棒的文档记录了它）。</p>
<p>在网络策略实现方面，通过实施Ingress和Egress规则，Calico，Canal，Cilium和WeaveNet是最好的控制面板。Kube-router实际上只实现了Ingress规则。</p>
<p>Flannel没有实现网络策略。</p>
<p>以下是结果摘要：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_8IXpUtCARPmm1W91nzWAxw.png" alt="img{512x368}" /></p>
<h2>性能</h2>
<p>该基准测试显示每次测试的三次运行（至少）的平均带宽。我们正在测试TCP和UDP性能（使用iperf3），真实应用程序，如HTTP（使用Nginx和curl），或FTP（使用vsftpd和curl），最后是使用SCP协议进行应用程序加密的行为（使用OpenSSH服务器和客户端）。</p>
<p>对于所有测试，我们还在裸机节点（绿色条）上运行基准测试，以比较CNI与本机网络性能的有效性。为了与我们的基准比例保持一致，我们在图表上使用以下颜色：</p>
<ul>
<li>黄色=非常好</li>
<li>橙色=好</li>
<li>蓝色=一般</li>
<li>红色=差</li>
</ul>
<p>因为我们不关注错误配置的CNI的性能，所以我们只会显示MTU调整的CNI基准测试结果。（NOTA BENE：如果激活加密，Cilium无法正确计算MTU，因此您必须在v1.4中手动将MTU降低到8900.下一版1.5将自动适应。）</p>
<p>结果如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_kuH6qDyQqqG7Nnex9PT4dg.png" alt="img{512x368}" /></p>
<p>每个CNI都在TCP基准测试中表现良好。由于加密成本，启用加密的CNI远远落后于其他CNI。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_68nsITzOCSYw829nH0doTQ.png" alt="img{512x368}" /></p>
<p>同样，在UDP基准测试中，所有CNI都表现良好。加密的CNI现在彼此非常接近。Cilium落后于其竞争对手，但事实上，它仅略高于裸机结果的2,3％，这是公平的。我们应该记住的是，Cilium和Flannel都是唯一能够正确自动检测MTU的CNI，从而提供了开箱即用的结果。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_8i95V9158G7EzMdwGuWdrQ.png" alt="img{512x368}" /></p>
<p>真实世界的应用程序怎么样？使用HTTP基准测试，我们可以看到全局性能略低于TCP测试。即使HTTP支持TCP，在TCP基准测试中，iperf3配置为避免任何“TCP慢启动”副作用，这可以有效地影响HTTP基准测试。这里的每个选手的表现都相当不错，Kube-router有明显的优势，WeaveNet在这项测试中表现非常糟糕，比裸机少了约20％。Cilium加密和WeaveNet加密现在都远远落后于裸机性能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_Riy2SAc46FcYgUqyAXypPg.png" alt="img{512x368}" /></p>
<p>使用FTP，另一个TCP支持的协议，结果更加复杂。虽然Flannel和Kube-router的表现非常好，但是Calico，Canal和Cilium稍稍落后，在裸机速度下约为10％。WeaveNet与裸机性能相差甚远，差距为17>％。无论如何，WeaveNet的加密版本比Cilium加密的性能高出约40％。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_kv4J0qVlG7uEMlJEhw3RnQ.png" alt="img{512x368}" /></p>
<p>通过SCP，我们可以清楚地看到SSH协议的加密成本。大多数CNI表现良好，但WeaveNet再次落后于其他人。当然，由于双重加密成本（SSH加密+ CNI加密）。</p>
<p>以下是性能摘要总结：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_EcLeju0yLgd6CwKMxBqcaw.png" alt="img{512x368}" /></p>
<h2>资源消耗</h2>
<p>现在让我们比较这些CNI在负载很重的情况下处理所带来的资源消耗如何（在TCP 10Gbit传输期间）。在性能测试中，我们将CNI与裸金属（绿色条）进行比较。对于资源消耗测试，我们还显示了没有任何CNI设置的新闲置Kubernetes（紫色条）的消耗。然后我们可以计算出CNI真正消耗的开销。</p>
<p>让我们从内存方面开始吧。以下是传输期间以MB为单位的平均节点RAM使用率（无缓冲区/缓存）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_GnPPDLyxIBbASVCXb_c1BA.png" alt="img{512x368}" /></p>
<p>Flannel和Kube-router表现非常好，只有大约50MB的内存占用，其次是Calico和Canal，70MB。WeaveNet的消费量明显高于其竞争对手，资源占用约为130MB。凭借400MB的内存占用，Cilium具有最高的基准内存消耗。</p>
<p>现在，让我们检查CPU消耗。警告：图形单位不是百分比，而是permil。因此裸金属的38 permil实际上是3.8％。结果如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_le6lgtbArrW9x5bVkerUNQ.png" alt="img{512x368}" /></p>
<p>Calico，Canal，Flannel和Kube-router都非常高效的CPU使用，与没有CNI的kubernetes相比，开销仅多出2％。远远落后于WeaveNet，开销约为5％，然后是Cilium，CPU开销超过7％。</p>
<p>以下是资源消耗的摘要：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_2Hj9aTvF-tJ6A90sfIlFTw.png" alt="img{512x368}" /></p>
<h2>摘要</h2>
<p>以下是所有结果的汇总概述：</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_vN2PBlNLOs--eHgNUuKHCg.png" alt="img{512x368}" /></p>
<h2>结论</h2>
<p>最后一部分是主观的，并传达了我自己对结果的解释。请记住，此基准测试仅在一个非常小的集群（3个节点）上测试单个连接中的吞吐速度。它不反映大型集群（> 50个节点）的网络行为，也没有多少连接并发。</p>
<p>如果你在相应的场景中，我建议使用以下CNI：</p>
<ul>
<li>您的群集中有低资源节点（只有几GB的RAM，几个核心）并且您不需要安全功能，请使用Flannel。它是我们测试过的最精简的CNI之一。此外，它与大量架构兼容（amd64，arm，arm64等）。它是唯一一个能够正确自动检测MTU的CNI，和Cilium一起，因此您无需配置任何内容即可使其正常工作。Kube-router也很好，但标准较低，需要您手动设置MTU。</li>
<li>出于安全原因，您需要加密网络，请使用WeaveNet。如果您使用巨型帧并通过在环境变量中提供密码来激活加密，请不要忘记设置MTU大小。但话说回过来，忘掉性能，这就是加密的代价。</li>
<li>对于其他常见用法，我会推荐Calico。这种CNI广泛用于许多kubernetes部署工具（Kops，Kubespray，Rancher等）。就像WeaveNet一样，如果您使用的是巨型帧，请不要忘记在ConfigMap中设置MTU。事实证明，它在资源消耗，性能和安全性方面具有多用途和高效性。</li>
</ul>
<p>最后但并非最不重要的，我建议你关注Cilium的工作。他们的团队非常活跃，他们正在努力提高他们的CNI（功能，资源节约，性能，安全性，多集群跨越&#8230;&#8230;），他们的路线图听起来非常有趣。</p>
<p><img src="https://tonybai.com/wp-content/uploads/1_WRQ8B9lcHL2LlImVbppoEQ.png" alt="img{512x368}" /></p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Hello，Termux</title>
		<link>https://tonybai.com/2017/11/09/hello-termux/</link>
		<comments>https://tonybai.com/2017/11/09/hello-termux/#comments</comments>
		<pubDate>Thu, 09 Nov 2017 13:51:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AArch64]]></category>
		<category><![CDATA[addon]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[apk4fun]]></category>
		<category><![CDATA[apt]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[connectbot]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[F-Droid]]></category>
		<category><![CDATA[geek]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.9.2]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hacker's-keyboard]]></category>
		<category><![CDATA[id_rsa.pub]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[logitech]]></category>
		<category><![CDATA[Lua]]></category>
		<category><![CDATA[MIX2]]></category>
		<category><![CDATA[neocomplete]]></category>
		<category><![CDATA[oh-my-zsh]]></category>
		<category><![CDATA[openssh]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[root]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[sources.list]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sshd]]></category>
		<category><![CDATA[terminal]]></category>
		<category><![CDATA[terminal-emulator]]></category>
		<category><![CDATA[Terminal-Emulator-for-Android]]></category>
		<category><![CDATA[termux]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[ultisnips]]></category>
		<category><![CDATA[UTF8]]></category>
		<category><![CDATA[Vim]]></category>
		<category><![CDATA[vim-go]]></category>
		<category><![CDATA[vim8.0]]></category>
		<category><![CDATA[vt100]]></category>
		<category><![CDATA[zsh]]></category>
		<category><![CDATA[安卓]]></category>
		<category><![CDATA[极客]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2454</guid>
		<description><![CDATA[程序员或多或少都有一颗Geek(极客)的心^0^。- Tony Bai 折腾开始。 这一切都源于前不久将手机换成了Xiaomi的MIX2。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用Android平台的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。 除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。 一、神器Termux，不仅仅是一个terminal emulator 所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding，还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的ConnectBot等ssh client端工具所无法提供的，而其他一些terminal工具，诸如Terminal Emulator for Android仅仅提供一些shell命令的支持，适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们，但对dev环境的搭建支持有限的。于是神器Termux登场了。 Termux是什么？Termux首先是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的Linux环境，你可以在无需root、无需root、无需root的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用apt工具进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。 1、安装 Termux仅支持Android 5.0及以上版本（估计现在绝大多数android机都满足这一条件）。在国内建议使用F-Droid安装Termux（先下载安装F-Droid，再在F-Droid内部搜索Termux，然后点击安装），国内的各种安装助手很少有对这个工具的支持。或是到apk4fun下载Termux的apk包（size非常小）到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为0.54。 在桌面点击安装后的Termux图标，我们就启动了一个Termux应用，见下图： 2、Termux初始环境探索 Mix2手机的Android系统使用的是Android 7.1.1版本，桌面Launcher用的是MIUI 9.1稳定版，默认的shell是bash。通过Termux，我们可以查看Android 7.1.1.使用的Linux内核版本如下： $uname -a Linux localhost 4.4.21-perf-g6a9ee37d-06186-g2b2a77b #1 SMP PREEMPT Thu Oct 26 14:55:45 CST 2017 aarch64 Android 可以看出Linux内核是4.4.21，采用的CPU arch family是ARM aarch64。 我再来看一下Termux提供的常见目录结构： Home路径： $cd ~/ $pwd [...]]]></description>
			<content:encoded><![CDATA[<p><strong><em>程序员或多或少都有一颗<a href="https://en.wikipedia.org/wiki/Geek">Geek(极客)</a>的心^0^。- Tony Bai</em></strong></p>
<p>折腾开始。</p>
<p>这一切都源于前不久将手机换成了Xiaomi的<a href="https://en.wikipedia.org/wiki/Xiaomi_Mi_MIX_2">MIX2</a>。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用<a href="https://en.wikipedia.org/wiki/Android_(operating_system)">Android平台</a>的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。</p>
<p>除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。</p>
<h2>一、神器Termux，不仅仅是一个terminal emulator</h2>
<p>所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding，还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的<a href="https://github.com/connectbot/connectbot">ConnectBot</a>等ssh client端工具所无法提供的，而其他一些terminal工具，诸如<a href="https://github.com/jackpal/Android-Terminal-Emulator">Terminal Emulator for Android</a>仅仅提供<a href="https://github.com/jackpal/Android-Terminal-Emulator/wiki/Android-Shell-Command-Reference">一些shell命令</a>的支持，适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们，但对dev环境的搭建支持有限的。于是神器<a href="https://termux.com/">Termux</a>登场了。</p>
<p><a href="https://github.com/termux/termux-app">Termux</a>是什么？Termux首先是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的<a href="http://tonybai.com/tag/linux">Linux</a>环境，你可以在<strong>无需root、无需root、无需root</strong>的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用<a href="https://en.wikipedia.org/wiki/APT_(Debian)">apt工具</a>进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。</p>
<h3>1、安装</h3>
<p>Termux仅<a href="https://github.com/termux/termux-app/issues/6">支持Android 5.0及以上版本</a>（估计现在绝大多数android机都满足这一条件）。在国内建议使用<a href="https://f-droid.org/packages/com.termux/">F-Droid</a>安装Termux（先下载安装F-Droid，再在F-Droid内部搜索Termux，然后点击安装），国内的各种安装助手很少有对这个工具的支持。或是到<a href="https://www.apk4fun.com/apk/74133/">apk4fun</a>下载Termux的apk包（size非常小）到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为<a href="https://github.com/termux/termux-app/releases/tag/v0.54">0.54</a>。</p>
<p>在桌面点击安装后的Termux图标，我们就启动了一个Termux应用，见下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-initial-start.jpg" alt="img{512x368}" /></p>
<h3>2、Termux初始环境探索</h3>
<p>Mix2手机的Android系统使用的是<a href="https://www.android.com/phones/">Android 7.1.1版本</a>，桌面Launcher用的是<a href="https://en.wikipedia.org/wiki/MIUI">MIUI 9.1</a>稳定版，默认的shell是<a href="http://tonybai.com/2009/02/27/make-bash-my-default-shell/">bash</a>。通过Termux，我们可以查看Android 7.1.1.使用的<a href="http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/">Linux内核</a>版本如下：</p>
<pre><code>$uname -a
Linux localhost 4.4.21-perf-g6a9ee37d-06186-g2b2a77b #1 SMP PREEMPT Thu Oct 26 14:55:45 CST 2017 aarch64 Android
</code></pre>
<p>可以看出<a href="http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/">Linux内核</a>是4.4.21，采用的CPU arch family是<a href="https://en.wikipedia.org/wiki/ARM_architecture">ARM</a> <a href="https://en.wikipedia.org/wiki/ARM_architecture#AArch64_features">aarch64</a>。</p>
<p>我再来看一下Termux提供的常见目录结构：</p>
<p>Home路径：</p>
<pre><code>$cd ~/
$pwd
/data/data/com.termux/files/home

//或者通过环境变量HOME获取：

$echo $HOME
/data/data/com.termux/files/home
</code></pre>
<p>长期使用Linux的朋友可能会发现，这个HOME路径好是奇怪，一般的标准<a href="https://en.wikipedia.org/wiki/Linux_distribution">Linux发行版</a>，比如<a href="http://tonybai.com/tag/ubuntu">Ubuntu</a>都是在”/home”下放置用户目录，但termux环境中HOME路径却是一个<strong>奇怪的位置</strong>。在<a href="https://wiki.termux.com/wiki/Main_Page">Termux官方Wiki</a>中，我们得到的答案是：Termux是一个prefixed system。</p>
<blockquote>
<p>这个prefix的含义我理解颇有些类似于我们在使用configure脚本时指定的&#8211;prefix参数的含义。我们在执行configure脚本时，如果不显式地给&#8211;prefix传入值，那么make install后，包将被install在<strong>标准位置</strong>；否则将被install在&#8211;prefix值所指定的位置。</p>
</blockquote>
<p>prefixed system意味着Termux中所有binaries、libraries、configs都不是放在标准的位置，比如：/usr/bin、/bin、/usr/lib、/etc等下面。Termux expose了一个特殊的环境变量:PREFIX（类似于configure &#8211;prefix参数选项)：</p>
<pre><code>$echo $PREFIX
/data/data/com.termux/files/usr

$cd $PREFIX
$ls -F
bin/  etc/  include/  lib/  libexec/  share/  tmp/  var/
</code></pre>
<p>是不是有些似曾相识？但Termux的$PREFIX路径与标准linux的根路径下的目录结构毕竟还<a href="https://wiki.termux.com/wiki/Differences_from_Linux">存在差别</a>，但有着对应关系，这种对应关系大致是：</p>
<pre><code>Termux的$PREFIX/bin  &lt;=&gt;  标准Linux环境的 /bin和/usr/bin
Termux的$PREFIX/lib  &lt;=&gt;  标准Linux环境的 /lib和/usr/lib
Termux的$PREFIX/var  &lt;=&gt;  标准Linux环境的 /var
Termux的$PREFIX/etc  &lt;=&gt;  标准Linux环境的 /etc
</code></pre>
<p>因此，基本可以认为Termux的$PREFIX/就对应于标准Linux的/路径。</p>
<h3>3、更新源和包管理</h3>
<p>Termux的牛逼之处在于它基于debian的<a href="https://en.wikipedia.org/wiki/APT_(Debian)">APT包</a>管理工具进行软件包的安装、管理和卸载，就像我们在Ubuntu下所做的那样，非常方便。</p>
<p>Termux自己<a href="http://termux.net/">维护了一个源</a>，提供各种专门为termux定制的包：</p>
<pre><code># The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main
</code></pre>
<p>同时，<a href="https://github.com/termux/termux-packages">termux-packages项目</a>为开发者和爱好者提供了构建工具和脚本，通过这些工具和脚本，我们可以将自己需要的软件包编译为可以在termux运行的版本，并补充到Termux的源之中。我大致测试了一下官方这个源还是可用的，虽然初始连接的响应很缓慢。</p>
<p>国内清华大学维护了一个<a href="https://mirror.tuna.tsinghua.edu.cn/help/termux/">Termux的镜像源</a>，你可以通过编辑 /data/data/com.termux/files/usr/etc/apt/sources.list文件或执行apt edit-sources命令编辑源(在Shell配置中添加export EDITOR=vi后，apt edit-sources才能启动编辑器进行编辑)：</p>
<pre><code># The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main
deb [arch=all,aarch64] http://mirrors.tuna.tsinghua.edu.cn/termux stable main
</code></pre>
<p>剩下的操作与Ubuntu上的一模一样，无非apt update后，利用apt install安装你想要的包。目前Termux源中都有哪些包呢？可以通过apt list命令查看：</p>
<pre><code>$apt list
Listing... Done
aapt/stable 7.1.2.33-1 aarch64
abduco/stable 0.6 aarch64
abook/stable 0.6.0pre2-1 aarch64
ack-grep/stable 2.18 all
alpine/stable 2.21 aarch64
angband/stable 4.1.0 aarch64
apache2/stable 2.4.29 aarch64
apache2-dev/stable 2.4.29 aarch64
apksigner/stable 0.4 all
apr/stable 1.6.3 aarch64
apr-dev/stable 1.6.3 aarch64
apr-util/stable 1.6.1 aarch64
apr-util-dev/stable 1.6.1 aarch64
apt/stable,now 1.2.12-3 aarch64 [installed]
apt-transport-https/stable 1.2.12-3 aarch64
... ...
zile/stable 2.4.14 aarch64
zip/stable 3.0-1 aarch64
zsh/stable,now 5.4.2-1 aarch64 [installed]
</code></pre>
<p>查看是否有需要更新的包列表：</p>
<pre><code>$apt list --upgradable
</code></pre>
<p>以安装<a href="http://tonybai.com/tag/go">golang</a>为例：</p>
<pre><code>$apt install golang
....
$go version
go version go1.9.2 android/arm64
</code></pre>
<p><img src="http://tonybai.com/wp-content/uploads/termux-apt-install-go.jpg" alt="img{512x368}" /></p>
<p>Termux源中的包似乎更新的很勤奋，<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9.2</a>才发布没多久，这里已经是最新版本了，这点值得赞一个！</p>
<h2>二、开发环境搭建</h2>
<p>我的目标是<strong>mobile coding</strong>，需要在Termux上搭建一个dev环境，以<a href="http://tonybai.com/tag/go">Go</a>环境为例。</p>
<h3>1、sshd</h3>
<p>在搭建和配置阶段，如果直接通过Android上的软键盘操作，即便屏再大，那个体验也是较差的。我们最好通过PC连到termux上去安装和配置，这就需要我们在Termux上搭建一个<a href="https://wiki.termux.com/wiki/SSH">sshd server</a>。下面是步骤：</p>
<pre><code>$apt install openssh
$sshd
</code></pre>
<p>就这么简单，一个sshd的server就在termux的后台启动起来了。由于Termux没有root权限，无法listen数值小于1024的端口，因此termux上sshd默认的listen端口是8022。另外termux上的sshd server不支持用户名+密码的方式进行登录，只能用免密登录的方式，即将PC上的~/.ssh/id_rsa.pub写入termux上的~/.ssh/authorized_keys文件中。关于免密登录的证书生成方法和导入方式，网上资料已经汗牛充栋，这里就不赘述了。导入PC端的id_rsa.pub后，PC就可以通过下面命令登录termux了：</p>
<pre><code>$ssh 10.88.46.79  -p 8022
Welcome to Termux!

Wiki:            https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel:     #termux on freenode
Gitter chat:     https://gitter.im/termux/termux
Mailing list:    termux+subscribe@groups.io

Search packages:   pkg search &lt;query&gt;
Install a package: pkg install &lt;package&gt;
Upgrade packages:  pkg upgrade
Learn more:        pkg help
</code></pre>
<p>其中10.88.46.79是手机的wlan0网卡的IP地址，可以在termux中使用ip addr命令获得:</p>
<pre><code>$ip addr show wlan0
34: wlan0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 3000
    ... ...
    inet 10.88.46.79/20 brd 10.88.47.255 scope global wlan0
       valid_lft forever preferred_lft forever
    ... ...
</code></pre>
<h3>2、定制shell</h3>
<p>Termux支持多种<a href="https://wiki.termux.com/wiki/Shells">主流Shell</a>，默认的Shell是<a href="http://tonybai.com/tag/bash">Bash</a>。很多开发者喜欢<a href="https://www.zsh.org/">zsh</a> + <a href="https://github.com/robbyrussell/oh-my-zsh">oh-my-zsh</a>的组合，Termux也是支持的，安装起来也是非常简单的：</p>
<pre><code>$ apt install git
$ apt install zsh
$ git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
$ cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
$ chsh zsh
</code></pre>
<p>与在PC上安装和配置zsh和oh-my-zsh没什么两样，你完全可以按照你在PC上的风格定制zsh的Theme等，我用的就是默认theme，所以也无需做太多变化，顶多定制一下PROMPT(~/.oh-my-zsh/themes/robbyrussell.zsh-theme中的PROMPT变量)的格式^0^。</p>
<h3>3、安装vim-go</h3>
<p>在terminal内进行Go开发，<a href="https://github.com/fatih/vim-go">vim-go</a>是必备之神器。vim-go以及相关自动补齐、snippet插件安装在不同平台上都是大同小异的，之前写过两篇《<a href="http://tonybai.com/2014/11/07/golang-development-environment-for-vim">Golang开发环境搭建-Vim篇</a>》和《<a href="http://tonybai.com/2016/09/08/upgrade-vim-go/">vim-go更新小记</a>》，大家可以参考。</p>
<p>不过这里有一个较为关键的问题，那就是Termux官方源中的vim 8.0缺少了对python和lua的支持：</p>
<pre><code> $vim --version|grep py
+cryptv          +linebreak       -python          +viminfo
+cscope          +lispindent      -python3         +vreplace
$vim --version|grep lua
+dialog_con      -lua             +rightleft       +windows
</code></pre>
<p>而一些插件又恰需要这些内置的支持，比如<a href="https://github.com/SirVer/ultisnips/issues/707">ultisnips</a>需要vim自带py支持；<a href="https://github.com/Shougo/neocomplete.vim">neocomplete</a>又依赖vim的lua支持。这样如果你还想要补齐和snippet特性，你就需要在Termux下面自己编译Vim的源码了（configure时加上对python和lua的支持）。</p>
<h3>4、中文支持</h3>
<p>无论是PC还是Termux使用的都是UTF8的内码格式，但是在安装完vim-go后，我试着用vim编辑一些简单的源码，发现在vim中输入的中文都是乱码。这里通过一个配置解决了该问题：</p>
<pre><code>//~/.vimrc

添加一行：

set enc=utf8

</code></pre>
<p>至于其中的原理，可以参见我N年前写的《<a href="http://tonybai.com/2009/09/28/also-talk-about-vim-charset-configuration/">也谈VIM字符集编码设置</a>》一文。</p>
<h2>三、键盘适配</h2>
<p>现阶段，写代码还是需要键盘输入的（憧憬未来^0^）。</p>
<h3>1、软键盘</h3>
<p>使用原生自带的默认软键盘在terminal中用vim进行coding，那得多执着啊，尤其是在vim大量使用ESC键的情况下（我都没找到原生键盘中ESC键在哪里:(）。不过Termux倒是很具包容心，为原生软键盘提供了扩展支持：用两个上下音量键协助你输入一些原生键盘上没有或者难于输入的符号，比如（全部的模拟按键列表参见<a href="https://wiki.termux.com/wiki/Touch_Keyboard">这里</a>）：</p>
<pre><code>清理屏幕：用volume down + L 来模拟 ctrl + L
结束前台程序：用volume down + C 来模拟 ctrl + C
ESC：用volume up + E 来模拟
F1-F9: 用volume up + 1 ~ 9 来模拟

</code></pre>
<blockquote>
<p>据网友提示：volume up + Q键可以打开扩展键盘键，包括ESC、CTRL、ALT等，感谢。</p>
</blockquote>
<p>这样仅能满足临时的需要，要想更有效率的输入，我们需要<a href="https://github.com/klausw/hackerskeyboard">Hacker&#8217;s Keyboard</a>。顾名思义，Hacker&#8217;s Keyboard可以理解为专为Coding(无论出于何种目的)的人准备的。和Termux一样，你可以从<a href="https://f-droid.org/packages/org.pocketworkstation.pckeyboard/">F-droid</a>安装该工具。启动该app后，app界面上有明确的使用说明，如果依旧不明确，还可以查看这篇图文并茂的文章：《<a href="https://www.wikihow.com/Use-Hacker%27s-Keyboard">How to Use Hacker&#8217;s Keyboard</a>》。默认情况下，横屏时Hacker&#8217;s keyboard会使用”Full 5-row layout”，即全键盘，竖屏时，则是4-row layout。你可以通过“系统设置”中的“语言和输入法”配置中对其进行设置，让Hacker&#8217;s keyboard无论在横屏还是竖屏都采用全键盘（我们屏幕够大^0^）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-hackers-keyboard-landscape.jpg" alt="img{512x368}" /><br />
横屏</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-hackers-keyboard-portrait.jpg" alt="img{512x368}" /><br />
竖屏</p>
<p>Hacker&#8217;s Keyboard无法支持中文输入，这点是目前的缺憾，不过我个人写代码时绝少使用中文，该问题忽略不计。</p>
<h3>2、外接蓝牙键盘</h3>
<p>Hacker&#8217;s Keyboard虽然一定程度提升了Coding时的输入效率，但也仅是权宜之计，长时间大规模通过软键盘输入依旧不甚可取，外接键盘是必须的。对于手机而言，目前最好的外接连接方式就是蓝牙。蓝牙键盘市面上现在有很多种，我选择了老牌大厂<a href="https://en.wikipedia.org/wiki/Logitech">logitech</a>的<a href="https://www.logitech.com/en-us/product/multi-device-keyboard-k480">K480</a>。这款键盘缺点是便携性差点、按键有些硬，但按键大小适中；而那些超便携的蓝牙键盘普遍键帽太小，长时间Coding的体验是个问题。</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-mix2-logitech-k480.jpg" alt="img{512x368}" /></p>
<p>Termux对外接键盘的支持也是很好的，除了常规输入，通过键盘组合键Ctrl+Alt与其他字母的组合<a href="https://wiki.termux.com/wiki/Hardware_Keyboard">实现各种控制功能</a>，比如：</p>
<pre><code>ctrl + alt + c =&gt; 实现创建一个新的session；
ctrl + alt + 上箭头/下箭头 =&gt; 实现切换到上一个/下一个session的窗口；
ctrl + alt + f =&gt; 全屏
ctrl + alt +v =&gt; 粘贴
ctrl + alt + +/- =&gt; 实现窗口字体的放大/缩小

</code></pre>
<p>不过，外接键盘和Hacker&#8217;s keyboard有一个相同的问题，那就是针对Termux无法输入中文。我尝试了百度、搜狗等输入法，无论如何切换（正常在其他应用中，通过【shift + 空格】实现中英文切换）均只是输入英文。</p>
<h2>四、存储</h2>
<p>到目前为止，我们提到的路径都在termux的私有的内部存储(private internal storage)路径下，这类存储的特点是termux应用内部的、私有的，一旦termux被卸载，这些数据也将不复存在。Android下还有另外两种存储类型：shared internal storage和external storage。所谓shared internal storage是手机上所有App可以共享的存储空间，放在这个空间内的数据不会因为App被卸载掉而被删除掉；而外部存储(external storage)主要是指外部插入的SD Card的存储空间。</p>
<p>默认情况下，Termux只支持private internal storage，意味着你要做好数据备份，否则一旦误卸载termux，数据可就都丢失了;数据可以用git进行管理，并sync到云端。</p>
<p>Termux提供了一个名为<a href="https://github.com/termux/termux-packages/blob/master/packages/termux-tools/termux-setup-storage">termux-setup-storage</a>的工具，可以让你在Termux下访问和使用shared internal storage和external storage；该工具是<a href="https://github.com/termux/termux-packages/tree/master/packages/termux-tools">termux-tools</a>的一部分，你可以通过apt install termux-tools来安装这些工具。</p>
<p>执行termux-setup-storage(注意：这个命令只能在手机上执行才能弹出授权对话框，通过远程ssh登录后执行没有任何效果)时，手机会弹出一个对话框，让你确认授权：</p>
<p><img src="http://tonybai.com/wp-content/uploads/termux-setup-storage.jpg" alt="img{512x368}" /></p>
<p>一旦授权，termux-setup-storage就会在HOME目录下建立一个storage目录，该目录下的结构如下：</p>
<pre><code>➜  /data/data/com.termux/files/home $tree storage
storage
├── dcim -&gt; /storage/emulated/0/DCIM
├── downloads -&gt; /storage/emulated/0/Download
├── movies -&gt; /storage/emulated/0/Movies
├── music -&gt; /storage/emulated/0/Music
├── pictures -&gt; /storage/emulated/0/Pictures
└── shared -&gt; /storage/emulated/0

6 directories, 0 files
</code></pre>
<p>我们看到在我的termux下，termux-setup-storage在storage下建立了6个符号链接，其中shared指向shared internal storage的根目录，即/storage/emulated/0；其余几个分别指向shared下的若干功能目录，比如：相册、音乐、电影、下载等。我的手机没有插SD卡，可能也不支持（市面上大多数手机都已经不支持了），如果插了一张SD卡，那么termux-setup-storage还会在storage目录下j建立一个符号链接指向在external storage上的一个termux private folder。</p>
<p>现在你就可以把数据放在shared internal storage和external storage上了，当然你也可以在Termux下自由访问shared internal storage上的数据了。</p>
<h2>五、小结</h2>
<p>Termux还设计了支持扩展的Addon机制，支持通过各种Addon来丰富Termux功能，提升其能力，这些算是高级功能，在这篇入门文章里就先不提及了。好了，接下来我就可以开始我的mobile coding了，充分利用碎片时间。后续在使用Termux+k480的过程中如果遇到什么具体的问题，我再来做针对性的解析。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/09/hello-termux/feed/</wfw:commentRss>
		<slash:comments>19</slash:comments>
		</item>
	</channel>
</rss>
