// Package ed25519x 基于标准库 crypto/ed25519 提供 Ed25519 数字签名的 // 密钥生成、PEM 编解码、签名与验签能力。 // // 安全说明: // - Ed25519 是确定性签名算法,不使用随机数,因此不存在 ECDSA 中"随机 // nonce 重复导致私钥泄露"的风险,也不需要调用方选择曲线或哈希算法。 // 它是新系统的推荐默认签名算法。 // - 私钥固定为 64 字节(32 字节种子 || 32 字节公钥)。私钥泄露等同于身份 // 被完全冒充,请仅存放在可信位置;本包不会把私钥写入日志或错误信息, // 也不提供自动清零,调用方使用完毕后应自行零覆盖相关内存。 // - 本包只做签名/验签,不提供加密与密钥协商;Ed25519 的签名结果不能当作 // 对称密钥或随机数使用。 // - 签名只保证完整性与来源真实性,不提供保密性:消息本身仍可能被读取。 package ed25519x import ( "crypto/ed25519" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "os" ) const ( // PublicKeySize Ed25519 公钥的固定字节长度(32)。 PublicKeySize = ed25519.PublicKeySize // PrivateKeySize Ed25519 私钥的固定字节长度(64,即 32 字节种子加 32 字节公钥)。 PrivateKeySize = ed25519.PrivateKeySize // SignatureSize Ed25519 签名的固定字节长度(64)。 SignatureSize = ed25519.SignatureSize ) // PEM 块类型标签,解析时严格校验,避免把别的算法或别的对象当成密钥使用。 const ( pemTypePrivateKey = "PRIVATE KEY" pemTypePublicKey = "PUBLIC KEY" ) // 文件权限:私钥仅所有者可读写,公钥可被其他用户读取。 const ( privateFileMode os.FileMode = 0o600 publicFileMode os.FileMode = 0o644 ) // GenerateKey 生成一对新的 Ed25519 密钥。 // // 返回值均为原始字节,长度分别恒为 PublicKeySize 与 PrivateKeySize; // privateKey 是敏感材料,不要打印到日志、不要经网络发送,也不要以明文落盘。 // // 返回的 publicKey 与 privateKey[32:] 相同,调用方只需保存 privateKey // 即可,但本包不做裁剪,保持与标准库一致的 64 字节形态。 func GenerateKey() (publicKey, privateKey []byte, err error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, nil, fmt.Errorf("生成 Ed25519 密钥失败: %w", err) } return []byte(pub), []byte(priv), nil } // GenerateKeyPEM 生成一对新的 Ed25519 密钥并以 PEM 文本返回。 // // privatePEM 为 PKCS#8 编码(块类型 "PRIVATE KEY"),publicPEM 为 PKIX // 编码(块类型 "PUBLIC KEY"),二者可直接写入 .pem 文件。 // // 安全提醒:privatePEM 是明文私钥,落盘时必须使用 0600 之类的严格权限 // (可参考 SaveKeyPair),不要把私钥 PEM 提交到版本库或打进镜像层。 func GenerateKeyPEM() (privatePEM, publicPEM []byte, err error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, nil, fmt.Errorf("生成 Ed25519 密钥失败: %w", err) } privatePEM, err = MarshalPrivateKeyPEM(priv) if err != nil { return nil, nil, err } publicPEM, err = MarshalPublicKeyPEM(pub) if err != nil { return nil, nil, err } return privatePEM, publicPEM, nil } // MarshalPrivateKeyPEM 把 Ed25519 私钥编码为 PKCS#8 的 PEM 文本。 // // 私钥长度不是 PrivateKeySize 时返回错误,不会 panic。 // 返回值是未加密的明文私钥,请按私钥要求保护。 func MarshalPrivateKeyPEM(priv ed25519.PrivateKey) ([]byte, error) { if len(priv) != PrivateKeySize { return nil, fmt.Errorf("Ed25519 私钥长度非法: 期望 %d 字节, 实际 %d 字节", PrivateKeySize, len(priv)) } der, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return nil, fmt.Errorf("PKCS#8 编码 Ed25519 私钥失败: %w", err) } return pem.EncodeToMemory(&pem.Block{Type: pemTypePrivateKey, Bytes: der}), nil } // MarshalPublicKeyPEM 把 Ed25519 公钥编码为 PKIX 的 PEM 文本。 // // 公钥长度不是 PublicKeySize 时返回错误,不会 panic。 func MarshalPublicKeyPEM(pub ed25519.PublicKey) ([]byte, error) { if len(pub) != PublicKeySize { return nil, fmt.Errorf("Ed25519 公钥长度非法: 期望 %d 字节, 实际 %d 字节", PublicKeySize, len(pub)) } der, err := x509.MarshalPKIXPublicKey(pub) if err != nil { return nil, fmt.Errorf("PKIX 编码 Ed25519 公钥失败: %w", err) } return pem.EncodeToMemory(&pem.Block{Type: pemTypePublicKey, Bytes: der}), nil } // ParsePrivateKeyPEM 解析 PKCS#8 的 Ed25519 私钥 PEM。 // // 只接受块类型为 "PRIVATE KEY" 的 PEM;块缺失、DER 非法、长度不符,或内容 // 是 RSA/ECDSA 等其他算法时均返回错误(错误文本只包含类型与长度,不含密钥材料)。 // 加密的 PKCS#8("ENCRYPTED PRIVATE KEY")不被支持。 func ParsePrivateKeyPEM(data []byte) (ed25519.PrivateKey, error) { block, _ := pem.Decode(data) if block == nil { return nil, errors.New("解析 Ed25519 私钥 PEM 失败: 数据不是合法的 PEM") } if block.Type != pemTypePrivateKey { return nil, fmt.Errorf("解析 Ed25519 私钥 PEM 失败: PEM 块类型为 %q, 期望 %q(不支持加密私钥或其他算法私钥)", block.Type, pemTypePrivateKey) } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("解析 PKCS#8 私钥失败: %w", err) } priv, ok := key.(ed25519.PrivateKey) if !ok { return nil, fmt.Errorf("PEM 中的私钥类型为 %T, 不是 Ed25519 私钥", key) } if len(priv) != PrivateKeySize { return nil, fmt.Errorf("Ed25519 私钥长度非法: 期望 %d 字节, 实际 %d 字节", PrivateKeySize, len(priv)) } return priv, nil } // ParsePublicKeyPEM 解析 PKIX 的 Ed25519 公钥 PEM。 // // 只接受块类型为 "PUBLIC KEY" 的 PEM,并强制校验解析结果的具体类型与长度: // 传入 RSA/ECDSA 公钥、证书或截断数据都会返回错误,绝不会因类型断言而 panic。 func ParsePublicKeyPEM(data []byte) (ed25519.PublicKey, error) { block, _ := pem.Decode(data) if block == nil { return nil, errors.New("解析 Ed25519 公钥 PEM 失败: 数据不是合法的 PEM") } if block.Type != pemTypePublicKey { return nil, fmt.Errorf("解析 Ed25519 公钥 PEM 失败: PEM 块类型为 %q, 期望 %q", block.Type, pemTypePublicKey) } key, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("解析 PKIX 公钥失败: %w", err) } pub, ok := key.(ed25519.PublicKey) if !ok { return nil, fmt.Errorf("PEM 中的公钥类型为 %T, 不是 Ed25519 公钥", key) } if len(pub) != PublicKeySize { return nil, fmt.Errorf("Ed25519 公钥长度非法: 期望 %d 字节, 实际 %d 字节", PublicKeySize, len(pub)) } return pub, nil } // LoadPrivateKey 从文件读取并解析 PKCS#8 的 Ed25519 私钥。 // // 文件不存在、不可读或内容非法都返回错误。请确保该路径的访问权限受控, // 私钥内容不会出现在任何返回值或日志里。 func LoadPrivateKey(path string) (ed25519.PrivateKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取 Ed25519 私钥文件 %q 失败: %w", path, err) } return ParsePrivateKeyPEM(data) } // LoadPublicKey 从文件读取并解析 PKIX 的 Ed25519 公钥。 func LoadPublicKey(path string) (ed25519.PublicKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取 Ed25519 公钥文件 %q 失败: %w", path, err) } return ParsePublicKeyPEM(data) } // SaveKeyPair 生成一对新的 Ed25519 密钥并写入两个文件。 // // privatePath 以 0600 权限写入(PKCS#8 PEM),publicPath 以 0644 权限写入 // (PKIX PEM);已存在的同名文件会被覆盖。若公钥写入失败,会尽力删除刚写入的 // 私钥文件以免留下不一致的残片。注意:在 Windows 上 os.WriteFile 的权限位只 // 影响只读属性,私钥文件的访问控制需依赖目录权限另行加固。 func SaveKeyPair(privatePath, publicPath string) error { privatePEM, publicPEM, err := GenerateKeyPEM() if err != nil { return err } if err := os.WriteFile(privatePath, privatePEM, privateFileMode); err != nil { return fmt.Errorf("写入 Ed25519 私钥文件 %q 失败: %w", privatePath, err) } if err := os.WriteFile(publicPath, publicPEM, publicFileMode); err != nil { if removeErr := os.Remove(privatePath); removeErr != nil { return fmt.Errorf("写入 Ed25519 公钥文件 %q 失败: %w(回滚私钥文件 %q 也失败: %v)", publicPath, err, privatePath, removeErr) } return fmt.Errorf("写入 Ed25519 公钥文件 %q 失败: %w(已回滚私钥文件)", publicPath, err) } return nil } // Sign 使用 Ed25519 私钥对 message 签名,返回 SignatureSize 字节的原始签名。 // // message 可以为空或任意长度,Ed25519 内部会先做哈希,调用方无需自行预哈希。 // 私钥长度不等于 PrivateKeySize(含 nil)时返回错误,不会 panic。 // 签名是确定性的:同一私钥与同一消息总是得到相同签名,便于做可复现的比对。 func Sign(priv ed25519.PrivateKey, message []byte) ([]byte, error) { if len(priv) != PrivateKeySize { return nil, fmt.Errorf("Ed25519 私钥长度非法: 期望 %d 字节, 实际 %d 字节", PrivateKeySize, len(priv)) } return ed25519.Sign(priv, message), nil } // Verify 校验 signature 是否为 pub 对 message 的有效 Ed25519 签名。 // // 校验通过返回 nil,否则返回错误。签名长度不是 SignatureSize、公钥长度不是 // PublicKeySize(含 nil)时同样返回错误:标准库 ed25519.Verify 对错误长度会 // panic,本函数在调用前已完成长度校验。 // // 安全提醒:返回的错误不区分"消息被篡改"与"签名被篡改",以免给攻击者提供 // 额外信息;验签失败只应表现为"拒绝",不要据此重试或降级到其他算法。 func Verify(pub ed25519.PublicKey, message, signature []byte) error { if len(pub) != PublicKeySize { return fmt.Errorf("Ed25519 公钥长度非法: 期望 %d 字节, 实际 %d 字节", PublicKeySize, len(pub)) } if len(signature) != SignatureSize { return fmt.Errorf("Ed25519 签名长度非法: 期望 %d 字节, 实际 %d 字节", SignatureSize, len(signature)) } if !ed25519.Verify(pub, message, signature) { return errors.New("Ed25519 验签失败: 签名与消息或公钥不匹配") } return nil } // SignToBase64 对 message 签名并返回标准 Base64 编码的签名文本, // 便于放进 JSON、HTTP 头或数据库字段。 // // 私钥非法时返回错误;返回值不是密钥材料,可以安全传输。 func SignToBase64(priv ed25519.PrivateKey, message []byte) (string, error) { signature, err := Sign(priv, message) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(signature), nil } // VerifyFromBase64 校验标准 Base64 编码的签名。 // // signatureB64 非法 Base64、解码后长度不为 SignatureSize,或验签不通过时 // 均返回错误;公钥非法同样返回错误,不会 panic。 func VerifyFromBase64(pub ed25519.PublicKey, message []byte, signatureB64 string) error { signature, err := base64.StdEncoding.DecodeString(signatureB64) if err != nil { return fmt.Errorf("Base64 解码 Ed25519 签名失败: %w", err) } return Verify(pub, message, signature) }