Files
encryptx/ed25519x/ed25519x.go
T

271 lines
11 KiB
Go
Raw Normal View History

2026-09-30 15:49:12 +08:00
// 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)
}