
本文深入探讨go语言中`go.crypto/openpgp`库在进行openpgp用户id签名时生成“坏签名”的问题。核心原因是库内部`signuserid()`实现存在缺陷,错误地使用了密钥签名算法而非用户id签名算法。文章将分析问题根源,并提供解决方案,包括迁移至最新库版本和加强错误处理,以确保生成有效的pgp签名。
Go语言OpenPGP用户ID签名问题概述
在使用Go语言的go.crypto/openpgp库尝试使用私钥签名公钥(特别是其用户ID)时,开发者可能会遇到一个令人困惑的问题:生成的PGP签名在通过GnuPG等外部工具验证时,会被报告为“坏签名”(bad Signature)。这通常意味着签名数据不符合OpenPGP规范,或者使用了错误的算法或数据结构。
以下是一个典型的Go语言实现,旨在从ASCII armored格式的公钥和私钥中读取并进行用户ID签名:
package mainimport ( "bytes" "code.google.com/p/go.crypto/openpgp" "code.google.com/p/go.crypto/openpgp/armor" "code.google.com/p/go.crypto/openpgp/packet" "fmt")// SignPubKeyPKS 接收ASCII armored格式的公钥、私钥和私钥密码,返回ASCII armored格式的已签名公钥。func SignPubKeyPKS(asciiPub string, asciiPri string, pripwd string) (asciiSignedKey string) { // 获取私钥实体 _, priEnt := getPri(asciiPri, pripwd) // 获取公钥实体 _, pubEnt := getPub(asciiPub) // 提取用户ID字符串 usrIdstring := "" for _, uIds := range pubEnt.Identities { usrIdstring = uIds.Name break // 假设只有一个用户ID或只签名第一个 } fmt.Println("尝试签名用户ID:", usrIdstring) // 签名用户ID errSign := pubEnt.SignIdentity(usrIdstring, &priEnt, nil) if errSign != nil { fmt.Println("签名用户ID失败:", errSign.Error()) return "" } // 将签名后的公钥实体转换为ASCII armored格式 asciiSignedKey = PubEntToAsciiArmor(pubEnt) return asciiSignedKey}// getPub 从ASCII armored格式字符串中解析出公钥和实体func getPub(asciiPub string) (pubKey packet.PublicKey, retEntity openpgp.Entity) { read1 := bytes.NewReader([]byte(asciiPub)) entityList, errReadArm := openpgp.ReadArmoredKeyRing(read1) if errReadArm != nil { fmt.Println("读取公钥失败:", errReadArm.Error()) return } if len(entityList) == 0 { fmt.Println("未找到公钥实体") return } for _, pubKeyEntity := range entityList { if pubKeyEntity.PrimaryKey != nil { pubKey = *pubKeyEntity.PrimaryKey retEntity = *pubKeyEntity break } } return}// getPri 从ASCII armored格式字符串中解析出私钥和实体,并解密func getPri(asciiPri string, pripwd string) (priKey packet.PrivateKey, priEnt openpgp.Entity) { read1 := bytes.NewReader([]byte(asciiPri)) entityList, errReadArm := openpgp.ReadArmoredKeyRing(read1) if errReadArm != nil { fmt.Println("读取私钥失败:", errReadArm.Error()) return } if len(entityList) == 0 { fmt.Println("未找到私钥实体") return } for _, can_pri := range entityList { if can_pri.PrivateKey == nil { fmt.Println("实体中不包含私钥") continue } smPr := can_pri.PrivateKey retEntity := can_pri priKey = *smPr errDecr := priKey.Decrypt([]byte(pripwd)) if errDecr != nil { fmt.Println("解密私钥失败:", errDecr.Error()) return } retEntity.PrivateKey = &priKey priEnt = *retEntity break // 假设只有一个私钥或只处理第一个 } return}// PubEntToAsciiArmor 将openpgp.Entity转换为ASCII armored格式func PubEntToAsciiArmor(pubEnt openpgp.Entity) (asciiEntity string) { gotWriter := bytes.NewBuffer(nil) wr, errEncode := armor.Encode(gotWriter, openpgp.PublicKeyType, nil) if errEncode != nil { fmt.Println("编码Armor失败:", errEncode.Error()) return "" } errSerial := pubEnt.Serialize(wr) if errSerial != nil { fmt.Println("序列化公钥失败:", errSerial.Error()) } errClosing := wr.Close() if errClosing != nil { fmt.Println("关闭writer失败:", errClosing.Error()) } asciiEntity = gotWriter.String() return asciiEntity}func main() { // 示例用法(需要替换为实际的公钥、私钥和密码) // asciiPublicKey := `-----BEGIN PGP PUBLIC KEY BLOCK-----...` // asciiPrivateKey := `-----BEGIN PGP PRIVATE KEY BLOCK-----...` // privateKeyPassword := "your-password" // signedKey := SignPubKeyPKS(asciiPublicKey, asciiPrivateKey, privateKeyPassword) // if signedKey != "" { // fmt.Println("n--- Signed Public Key ---") // fmt.Println(signedKey) // } else { // fmt.Println("签名失败。") // }}
上述代码逻辑上看起来合理,但却无法生成一个被GnuPG认可的有效签名。
问题根源分析:库内部实现缺陷
经过深入调查,发现此问题的根本原因在于code.google.com/p/go.crypto/openpgp库的内部实现缺陷,而非开发者代码逻辑错误。具体来说:
立即学习“go语言免费学习笔记(深入)”;
错误的签名算法选择:openpgp库中用于签名用户ID的方法(例如Signature.SignUserId()或通过pubEnt.SignIdentity间接调用)错误地使用了用于密钥签名的算法和数据结构。OpenPGP规范中,用户ID签名和密钥签名(用于认证子密钥属于主密钥)是两种不同的操作,需要生成不同类型的签名包(Packet Tag 2, Certification Signature Packet)。不正确的哈希上下文:在验证用户ID签名时,库的PublicKey.VerifyUserIdSignature()方法也存在问题,它没有使用正确的公钥作为哈希上下文,导致只能验证自签名用户ID,而无法正确验证由其他密钥签名的用户ID。
这些问题导致库生成的用户ID签名不符合OpenPGP标准,因此会被GnuPG等严格遵循标准的工具识别为无效。
官方Bug报告与补丁
此问题在Go社区中已被识别并报告。相关的官方Bug报告可以在https://www.php.cn/link/99c3c828637e01c4337451ab836f62ef找到,其中也包含了针对该缺陷的补丁。这表明该问题是openpgp库早期版本的一个已知且已修复的内部错误。
解决方案与最佳实践
鉴于问题根源在于库本身的实现缺陷,解决此问题的关键在于升级和迁移到已修复该bug的库版本。
1. 升级并迁移至最新库版本
code.google.com/p/go.crypto/openpgp是Go语言加密库的旧路径,已被弃用。官方推荐和维护的加密库路径是golang.org/x/crypto/openpgp。
推荐操作步骤:
更新依赖:将项目中的code.google.com/p/go.crypto/openpgp及相关子包的导入路径全部替换为golang.org/x/crypto/openpgp。获取最新版本:确保您的Go模块依赖已更新到golang.org/x/crypto的最新稳定版本。您可以使用以下命令更新:
go get -u golang.org/x/crypto/openpgp
并确保您的go.mod文件反映了最新的版本。
golang.org/x/crypto/openpgp的最新版本已经包含了对Bug 7371的修复,能够正确生成和验证用户ID签名。迁移后,原有的签名代码(如示例中的SignPubKeyPKS函数)在逻辑上将能正常工作,生成符合OpenPGP规范的有效签名。
2. 增强错误处理
尽管核心问题是库的bug,但在任何生产级代码中,健壮的错误处理都是至关重要的。在上述示例代码中,虽然有一些错误打印,但对于某些关键错误路径(例如getPri或getPub未能找到实体),函数的返回值可能没有被充分利用或导致后续操作空指针。
改进建议:
对于可能返回nil或空切片的操作,应在返回前进行显式检查并返回有意义的错误。避免在函数内部直接fmt.Println错误信息并返回空值,而是将错误返回给调用者,由调用者决定如何处理(例如,日志记录、重试、向用户报告)。在处理openpgp.Entity或packet.PrivateKey时,始终检查其是否为nil。
例如,可以修改getPub和getPri函数,使其在失败时返回error:
// getPub 从ASCII armored格式字符串中解析出公钥和实体func getPub(asciiPub string) (pubKey packet.PublicKey, retEntity openpgp.Entity, err error) { read1 := bytes.NewReader([]byte(asciiPub)) entityList, errReadArm := openpgp.ReadArmoredKeyRing(read1) if errReadArm != nil { return packet.PublicKey{}, openpgp.Entity{}, fmt.Errorf("读取公钥失败: %w", errReadArm) } if len(entityList) == 0 { return packet.PublicKey{}, openpgp.Entity{}, fmt.Errorf("未找到公钥实体") } for _, pubKeyEntity := range entityList { if pubKeyEntity.PrimaryKey != nil { pubKey = *pubKeyEntity.PrimaryKey retEntity = *pubKeyEntity return pubKey, retEntity, nil } } return packet.PublicKey{}, openpgp.Entity{}, fmt.Errorf("未在实体列表中找到主公钥")}// getPri 从ASCII armored格式字符串中解析出私钥和实体,并解密func getPri(asciiPri string, pripwd string) (priKey packet.PrivateKey, priEnt openpgp.Entity, err error) { read1 := bytes.NewReader([]byte(asciiPri)) entityList, errReadArm := openpgp.ReadArmoredKeyRing(read1) if errReadArm != nil { return packet.PrivateKey{}, openpgp.Entity{}, fmt.Errorf("读取私钥失败: %w", errReadArm) } if len(entityList) == 0 { return packet.PrivateKey{}, openpgp.Entity{}, fmt.Errorf("未找到私钥实体") } for _, can_pri := range entityList { if can_pri.PrivateKey == nil { continue // 跳过没有私钥的实体 } smPr := can_pri.PrivateKey retEntity := can_pri priKey = *smPr errDecr := priKey.Decrypt([]byte(pripwd)) if errDecr != nil { return packet.PrivateKey{}, openpgp.Entity{}, fmt.Errorf("解密私钥失败: %w", errDecr) } retEntity.PrivateKey = &priKey priEnt = *retEntity return priKey, priEnt, nil } return packet.PrivateKey{}, openpgp.Entity{}, fmt.Errorf("未在实体列表中找到可用的私钥")}// SignPubKeyPKS 签名函数也应返回错误func SignPubKeyPKS(asciiPub string, asciiPri string, pripwd string) (asciiSignedKey string, err error) { _, priEnt, err := getPri(asciiPri, pripwd) if err != nil { return "", fmt.Errorf("获取私钥失败: %w", err) } _, pubEnt, err := getPub(asciiPub) if err != nil { return "", fmt.Errorf("获取公钥失败: %w", err) } usrIdstring := "" for _, uIds := range pubEnt.Identities { usrIdstring = uIds.Name break } if usrIdstring == "" { return "", fmt.Errorf("公钥实体中未找到用户ID") } fmt.Println("尝试签名用户ID:", usrIdstring) errSign := pubEnt.SignIdentity(usrIdstring, &priEnt, nil) if errSign != nil { return "", fmt.Errorf("签名用户ID失败: %w", errSign) } asciiSignedKey, err = PubEntToAsciiArmor(pubEnt) if err != nil { return "", fmt.Errorf("转换为ASCII Armor格式失败: %w", err) } return asciiSignedKey, nil}// PubEntToAsciiArmor 转换函数也应返回错误func PubEntToAsciiArmor(pubEnt openpgp.Entity) (asciiEntity string, err error) { gotWriter := bytes.NewBuffer(nil) wr, errEncode := armor.Encode(gotWriter, openpgp.PublicKeyType, nil) if errEncode != nil { return "", fmt.Errorf("编码Armor失败: %w", errEncode) } errSerial := pubEnt.Serialize(wr) if errSerial != nil { return "", fmt.Errorf("序列化公钥失败: %w", errSerial) } errClosing := wr.Close() if errClosing != nil { return "", fmt.Errorf("关闭writer失败: %w", errClosing) } return gotWriter.String(), nil}
3. 验证签名有效性
即使代码逻辑和库版本都已正确,在涉及加密操作时,始终建议进行严格的端到端测试。使用GnuPG等外部工具验证Go程序生成的PGP签名,是确保其符合标准和互操作性的最佳实践。
# 假设您已将Go程序生成的签名公钥保存到 signed_public_key.ascgpg --check-sigs signed_public_key.asc
如果签名有效,GnuPG将显示正确的签名信息,而不会报告“bad Signature”。
总结
Go语言openpgp库在早期版本中存在一个关键缺陷,导致其生成的用户ID签名被GnuPG等工具判定为无效。该问题源于库内部对OpenPGP规范的错误实现,尤其是在签名算法和哈希上下文的处理上。
解决此问题的核心在于:
迁移并升级:将项目中的code.google.com/p/go.crypto/openpgp依赖更新为golang.org/x/crypto/openpgp的最新稳定版本,该版本已包含了对相关bug的修复。强化错误处理:在Go代码中实现更健壮的错误检查和传播机制,以提高程序的稳定性和可维护性。外部验证:始终使用G
以上就是Go语言openpgp库用户ID签名无效问题深度解析与解决方案的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1425784.html
微信扫一扫
支付宝扫一扫