381 lines
14 KiB
Go
381 lines
14 KiB
Go
// 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)
|
||
}
|