
本文深入探讨go语言中`go.crypto/openpgp`库在进行openpgp用户id签名时生成“坏签名”的问题。核心原因是库内部`signuserid()`实现存在缺陷,错误地使用了密钥签名算法而非用户id签名算法。文章将分析问题根源,并提供解决方案,包括迁移至最新库版本和加强错误处理,以确保生成有效的pgp签名。
在使用Go语言的go.crypto/openpgp库尝试使用私钥签名公钥(特别是其用户ID)时,开发者可能会遇到一个令人困惑的问题:生成的PGP签名在通过GnuPG等外部工具验证时,会被报告为“坏签名”(bad Signature)。这通常意味着签名数据不符合OpenPGP规范,或者使用了错误的算法或数据结构。
以下是一个典型的Go语言实现,旨在从ASCII armored格式的公钥和私钥中读取并进行用户ID签名:
package main
import (
"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语言免费学习笔记(深入)”;
这些问题导致库生成的用户ID签名不符合OpenPGP标准,因此会被GnuPG等严格遵循标准的工具识别为无效。
官方Bug报告与补丁
此问题在Go社区中已被识别并报告。相关的官方Bug报告可以在https://www.php.cn/link/99c3c828637e01c4337451ab836f62ef找到,其中也包含了针对该缺陷的补丁。这表明该问题是openpgp库早期版本的一个已知且已修复的内部错误。
鉴于问题根源在于库本身的实现缺陷,解决此问题的关键在于升级和迁移到已修复该bug的库版本。
code.google.com/p/go.crypto/openpgp是Go语言加密库的旧路径,已被弃用。官方推荐和维护的加密库路径是golang.org/x/crypto/openpgp。
推荐操作步骤:
go get -u golang.org/x/crypto/openpgp
并确保您的go.mod文件反映了最新的版本。
golang.org/x/crypto/openpgp的最新版本已经包含了对Bug 7371的修复,能够正确生成和验证用户ID签名。迁移后,原有的签名代码(如示例中的SignPubKeyPKS函数)在逻辑上将能正常工作,生成符合OpenPGP规范的有效签名。
尽管核心问题是库的bug,但在任何生产级代码中,健壮的错误处理都是至关重要的。在上述示例代码中,虽然有一些错误打印,但对于某些关键错误路径(例如getPri或getPub未能找到实体),函数的返回值可能没有被充分利用或导致后续操作空指针。
改进建议:
例如,可以修改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
}即使代码逻辑和库版本都已正确,在涉及加密操作时,始终建议进行严格的端到端测试。使用GnuPG等外部工具验证Go程序生成的PGP签名,是确保其符合标准和互操作性的最佳实践。
# 假设您已将Go程序生成的签名公钥保存到 signed_public_key.asc gpg --check-sigs signed_public_key.asc
如果签名有效,GnuPG将显示正确的签名信息,而不会报告“bad Signature”。
Go语言openpgp库在早期版本中存在一个关键缺陷,导致其生成的用户ID签名被GnuPG等工具判定为无效。该问题源于库内部对OpenPGP规范的错误实现,尤其是在签名算法和哈希上下文的处理上。
解决此问题的核心在于:
以上就是Go语言openpgp库用户ID签名无效问题深度解析与解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号