Files
encryptx/ecdsax/ecdsax.go
T
2026-09-30 15:49:12 +08:00

381 lines
14 KiB
Go
Raw 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 ecdsax 基于标准库 crypto/ecdsa 提供 ECDSA 数字签名的密钥生成、
// PEM 编解码、签名与验签能力,支持 NIST P-256 / P-384 / P-521 三条曲线。
//
// 摘要算法按曲线自动选择,调用方无需(也不允许)自行指定:
//
// P-256 -> SHA-256
// P-384 -> SHA-384
// P-521 -> SHA-512
//
// 安全说明:
// - 仅接受标准库的三条 NIST 曲线单例(elliptic.P256/P384/P521),
// P-224 已不再安全、自定义曲线可能实现错误或被替换,一律拒绝。
// - Sign 内部使用 crypto/ecdsa 的加固实现:每个签名都消耗系统安全随机源
// 生成 nonce,调用方不提供也不能复用 nonce,从而避免 nonce 重复导致
// 私钥泄露(这是 ECDSA 最经典的事故来源)。
// - Verify 使用标准库实现,签名校验过程为常量时间语义,不会因签名的部分
// 字节正确而提前返回。
// - 私钥(*ecdsa.PrivateKey)泄露等同于身份被完全冒充;本包不会把私钥、
// 明文或随机数写入错误信息或标准输出,也不提供自动清零,调用方用完应
// 自行零覆盖 priv.D 等敏感值。
// - 本包只做签名/验签,不提供加密与密钥协商;如需加密请使用 AES-GCM 或
// RSA-OAEP 等方案。
// - ECDSA 的兼容性好,但签名非确定且有参数(曲线、哈希);新系统若没有
// 互操作要求,签名场景优先考虑 Ed25519(见包 ed25519x)。
package ecdsax
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"hash"
"os"
)
// PEM 块类型标签:PKCS#8 私钥、SEC1 私钥(兼容 OpenSSL ecparam 产物)、PKIX 公钥。
const (
pemTypePrivateKeyPKCS8 = "PRIVATE KEY"
pemTypePrivateKeySEC1 = "EC PRIVATE KEY"
pemTypePublicKey = "PUBLIC KEY"
pemTypePublicKeySEC1 = "EC PUBLIC KEY"
)
// 文件权限:私钥仅所有者可读写,公钥可被其他用户读取。
const (
privateFileMode os.FileMode = 0o600
publicFileMode os.FileMode = 0o644
)
// hashForCurve 返回曲线对应的摘要构造函数,同时完成曲线白名单校验。
//
// 用接口值相等(而非曲线名字符串)判断,只接受标准库的曲线单例:名字相同
// 的自定义曲线可能伪装成 P-256 却使用可预测的运算,必须拒绝。
func hashForCurve(curve elliptic.Curve) (func() hash.Hash, error) {
switch curve {
case elliptic.P256():
return sha256.New, nil
case elliptic.P384():
return sha512.New384, nil
case elliptic.P521():
return sha512.New, nil
default:
return nil, fmt.Errorf("不支持的椭圆曲线 %s: 仅支持 P-256、P-384、P-521", curveName(curve))
}
}
// curveName 安全地取出曲线名,用于错误信息;任何情况下都不会因 nil 而 panic。
func curveName(curve elliptic.Curve) string {
if curve == nil {
return "<nil>"
}
if params := curve.Params(); params != nil {
return params.Name
}
return fmt.Sprintf("%T", curve)
}
// validatePrivateKey 校验私钥并返回其摘要构造函数。
//
// nil、曲线不受支持、坐标或私钥标量缺失、点不在曲线上等情形都返回错误;
// 校验放在签名之前,可避免底层实现遇到畸形密钥时 panic 或产生有泄漏风险的运算。
func validatePrivateKey(priv *ecdsa.PrivateKey) (func() hash.Hash, error) {
if priv == nil {
return nil, errors.New("ECDSA 私钥为 nil")
}
newHash, err := hashForCurve(priv.Curve)
if err != nil {
return nil, err
}
if priv.D == nil {
return nil, errors.New("非法的 ECDSA 私钥: 缺少私钥标量 D")
}
if priv.X == nil || priv.Y == nil {
return nil, errors.New("非法的 ECDSA 私钥: 缺少公钥坐标 X/Y")
}
if _, err := priv.ECDH(); err != nil {
return nil, fmt.Errorf("非法的 ECDSA 私钥: %w", err)
}
return newHash, nil
}
// validatePublicKey 校验公钥并返回其摘要构造函数。
//
// nil、曲线不受支持、坐标缺失、点不在曲线上或为无穷远点等情形都返回错误;
// 校验放在验签之前,可避免底层实现对畸形公钥 panic。
func validatePublicKey(pub *ecdsa.PublicKey) (func() hash.Hash, error) {
if pub == nil {
return nil, errors.New("ECDSA 公钥为 nil")
}
newHash, err := hashForCurve(pub.Curve)
if err != nil {
return nil, err
}
if pub.X == nil || pub.Y == nil {
return nil, errors.New("非法的 ECDSA 公钥: 缺少坐标 X/Y")
}
if _, err := pub.ECDH(); err != nil {
return nil, fmt.Errorf("非法的 ECDSA 公钥: %w", err)
}
return newHash, nil
}
// GenerateKey 生成一条指定曲线的 ECDSA 私钥。
//
// curve 只能是 elliptic.P256()、elliptic.P384() 或 elliptic.P521();传入 nil、
// P-224 或自定义曲线返回错误(P-224 的安全强度已不足,且标准库在部分模式下
// 已不再支持)。私钥包含公钥,请按私钥级别保护返回值。
func GenerateKey(curve elliptic.Curve) (*ecdsa.PrivateKey, error) {
if _, err := hashForCurve(curve); err != nil {
return nil, err
}
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, fmt.Errorf("生成 ECDSA 私钥失败: %w", err)
}
return priv, nil
}
// GenerateKeyPEM 生成一条指定曲线的 ECDSA 密钥对并以 PEM 文本返回。
//
// privatePEM 为 PKCS#8 编码("PRIVATE KEY"),publicPEM 为 PKIX 编码
// ("PUBLIC KEY")。privatePEM 是明文私钥,落盘时务必使用严格权限。
func GenerateKeyPEM(curve elliptic.Curve) (privatePEM, publicPEM []byte, err error) {
priv, err := GenerateKey(curve)
if err != nil {
return nil, nil, err
}
privatePEM, err = MarshalPrivateKeyPEM(priv)
if err != nil {
return nil, nil, err
}
publicPEM, err = MarshalPublicKeyPEM(&priv.PublicKey)
if err != nil {
return nil, nil, err
}
return privatePEM, publicPEM, nil
}
// MarshalPrivateKeyPEM 把 ECDSA 私钥编码为 PKCS#8 的 PEM 文本。
//
// priv 为 nil、曲线不受支持或密钥字段不完整时返回错误,不会 panic。
// 返回值是未加密的明文私钥,请按私钥要求保护。
func MarshalPrivateKeyPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
if _, err := validatePrivateKey(priv); err != nil {
return nil, err
}
der, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, fmt.Errorf("PKCS#8 编码 ECDSA 私钥失败: %w", err)
}
return pem.EncodeToMemory(&pem.Block{Type: pemTypePrivateKeyPKCS8, Bytes: der}), nil
}
// MarshalPublicKeyPEM 把 ECDSA 公钥编码为 PKIX 的 PEM 文本。
//
// pub 为 nil、曲线不受支持或坐标非法时返回错误,不会 panic。
func MarshalPublicKeyPEM(pub *ecdsa.PublicKey) ([]byte, error) {
if _, err := validatePublicKey(pub); err != nil {
return nil, err
}
der, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("PKIX 编码 ECDSA 公钥失败: %w", err)
}
return pem.EncodeToMemory(&pem.Block{Type: pemTypePublicKey, Bytes: der}), nil
}
// ParsePrivateKeyPEM 解析 ECDSA 私钥 PEM。
//
// 接受 PKCS#8("PRIVATE KEY")与 SEC1("EC PRIVATE KEY",OpenSSL 传统格式)
// 两种块类型;块缺失、DER 非法、内容是别的算法,或曲线为 P-224/自定义曲线时
// 均返回错误。错误文本只包含类型、块类型与曲线名,不含任何私钥材料。
func ParsePrivateKeyPEM(data []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("解析 ECDSA 私钥 PEM 失败: 数据不是合法的 PEM")
}
var (
priv *ecdsa.PrivateKey
err error
)
switch block.Type {
case pemTypePrivateKeyPKCS8:
var key any
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析 PKCS#8 私钥失败: %w", err)
}
parsed, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("PEM 中的私钥类型为 %T, 不是 ECDSA 私钥", key)
}
priv = parsed
case pemTypePrivateKeySEC1:
priv, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析 SEC1 EC 私钥失败: %w", err)
}
default:
return nil, fmt.Errorf("不支持解析 ECDSA 私钥的 PEM 块类型 %q: 期望 %q 或 %q", block.Type, pemTypePrivateKeyPKCS8, pemTypePrivateKeySEC1)
}
if _, err := validatePrivateKey(priv); err != nil {
return nil, err
}
return priv, nil
}
// ParsePublicKeyPEM 解析 PKIX 的 ECDSA 公钥 PEM。
//
// 只接受块类型 "PUBLIC KEY"(以及 OpenSSL 早期输出的 "EC PUBLIC KEY",其
// DER 仍是 PKIX),并强制校验解析结果的具体类型与曲线:传入 RSA/Ed25519 公钥、
// 证书或截断数据都会返回错误,绝不会因非受检类型断言而 panic。
func ParsePublicKeyPEM(data []byte) (*ecdsa.PublicKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("解析 ECDSA 公钥 PEM 失败: 数据不是合法的 PEM")
}
if block.Type != pemTypePublicKey && block.Type != pemTypePublicKeySEC1 {
return nil, fmt.Errorf("不支持解析 ECDSA 公钥的 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.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("PEM 中的公钥类型为 %T, 不是 ECDSA 公钥", key)
}
if _, err := validatePublicKey(pub); err != nil {
return nil, err
}
return pub, nil
}
// LoadPrivateKey 从文件读取并解析 ECDSA 私钥。
//
// 文件不存在、不可读或内容非法都返回错误。请确保该路径的访问权限受控,
// 私钥内容不会出现在任何返回值或日志里。
func LoadPrivateKey(path string) (*ecdsa.PrivateKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取 ECDSA 私钥文件 %q 失败: %w", path, err)
}
return ParsePrivateKeyPEM(data)
}
// LoadPublicKey 从文件读取并解析 PKIX 的 ECDSA 公钥。
func LoadPublicKey(path string) (*ecdsa.PublicKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取 ECDSA 公钥文件 %q 失败: %w", path, err)
}
return ParsePublicKeyPEM(data)
}
// SaveKeyPair 生成一条指定曲线的新 ECDSA 密钥并写入两个文件。
//
// privatePath 以 0600 权限写入(PKCS#8 PEM),publicPath 以 0644 权限写入
// (PKIX PEM);已存在的同名文件会被覆盖。若公钥写入失败,会尽力删除刚写入的
// 私钥文件以免留下不一致的残片。注意:在 Windows 上 os.WriteFile 的权限位只
// 影响只读属性,私钥文件的访问控制需依赖目录权限另行加固。
func SaveKeyPair(privatePath, publicPath string, curve elliptic.Curve) error {
privatePEM, publicPEM, err := GenerateKeyPEM(curve)
if err != nil {
return err
}
if err := os.WriteFile(privatePath, privatePEM, privateFileMode); err != nil {
return fmt.Errorf("写入 ECDSA 私钥文件 %q 失败: %w", privatePath, err)
}
if err := os.WriteFile(publicPath, publicPEM, publicFileMode); err != nil {
if removeErr := os.Remove(privatePath); removeErr != nil {
return fmt.Errorf("写入 ECDSA 公钥文件 %q 失败: %w(回滚私钥文件 %q 也失败: %v)", publicPath, err, privatePath, removeErr)
}
return fmt.Errorf("写入 ECDSA 公钥文件 %q 失败: %w(已回滚私钥文件)", publicPath, err)
}
return nil
}
// Sign 使用 ECDSA 私钥对 message 签名,返回 ASN.1 DER 编码的签名。
//
// 摘要算法由曲线自动决定(P-256→SHA-256,P-384→SHA-384,P-521→SHA-512),
// 不需要也不允许调用方预哈希。每次签名都会使用系统安全随机源生成新的 nonce,
// 因此同一消息的两次签名不同,这是正常现象。
//
// priv 为 nil、曲线不受支持或密钥字段非法时返回错误,不会 panic。
// DER 签名长度随曲线和随机值浮动(P-256 通常约 70~72 字节),
// 存储时应留足空间,不要按固定长度截断。
func Sign(priv *ecdsa.PrivateKey, message []byte) ([]byte, error) {
newHash, err := validatePrivateKey(priv)
if err != nil {
return nil, err
}
h := newHash()
if _, err := h.Write(message); err != nil {
return nil, fmt.Errorf("计算 ECDSA 摘要失败: %w", err)
}
signature, err := ecdsa.SignASN1(rand.Reader, priv, h.Sum(nil))
if err != nil {
return nil, fmt.Errorf("ECDSA 签名失败: %w", err)
}
return signature, nil
}
// Verify 校验 signature 是否为 pub 对 message 的有效 ECDSA 签名。
//
// signature 必须是 Sign 产生的 ASN.1 DER 编码签名;校验通过返回 nil,否则返回
// 错误。签名为空、DER 非法或被篡改,以及消息不匹配、公钥不匹配时都返回错误;
// pub 为 nil 或曲线不受支持时同样返回错误,不会 panic。
//
// 标准库的签名校验为常量时间语义,但错误信息不区分失败原因,以免向攻击者
// 泄露可用于伪造的信息;验签失败只应表现为"拒绝"。
func Verify(pub *ecdsa.PublicKey, message, signature []byte) error {
newHash, err := validatePublicKey(pub)
if err != nil {
return err
}
if len(signature) == 0 {
return errors.New("ECDSA 签名为空")
}
h := newHash()
if _, err := h.Write(message); err != nil {
return fmt.Errorf("计算 ECDSA 摘要失败: %w", err)
}
if !ecdsa.VerifyASN1(pub, h.Sum(nil), signature) {
return errors.New("ECDSA 验签失败: 签名与消息或公钥不匹配")
}
return nil
}
// SignToBase64 对 message 签名并返回标准 Base64 编码的 DER 签名文本,
// 便于放进 JSON、HTTP 头或数据库字段。私钥非法时返回错误。
func SignToBase64(priv *ecdsa.PrivateKey, message []byte) (string, error) {
signature, err := Sign(priv, message)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// VerifyFromBase64 校验标准 Base64 编码的 DER 签名。
//
// signatureB64 非法 Base64,或解码后验签不通过,或公钥非法时均返回错误,
// 不会 panic。
func VerifyFromBase64(pub *ecdsa.PublicKey, message []byte, signatureB64 string) error {
signature, err := base64.StdEncoding.DecodeString(signatureB64)
if err != nil {
return fmt.Errorf("Base64 解码 ECDSA 签名失败: %w", err)
}
return Verify(pub, message, signature)
}