Files
2026-09-30 15:49:12 +08:00

271 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}