pem 签名校验

背景:PEM 是什么

PEM 是把密钥 / 证书用 Base64 包一层、带 -----BEGIN ...----- 头尾的文本格式。Go 里用 encoding/pem 解码得到 block.Bytes(内部的 DER/ASN.1 字节),再用 crypto/x509 解析成具体的密钥对象。

ed25519 是 Edwards 曲线的数字签名算法,密钥只有 32 字节、速度快、无需复杂参数。下面把「PEM 解析 + ed25519 签名 / 校验」封装成一个 signer 包,分三块看:解析密钥、签名校验、以及怎么生成一对 PEM 密钥。

1. 解析 PEM 里的密钥

私钥走 PKCS#8(x509.ParsePKCS8PrivateKey),公钥走 PKIX(x509.ParsePKIXPublicKey)。pemParse 负责解码并强制校验 BEGIN 头里的类型是否匹配。

package signer

import (
	"crypto/ed25519"
	"crypto/x509"
	"encoding/pem"
	"fmt"
)

func parsePrivateKey(data []byte) (ed25519.PrivateKey, error) {
	pemData, err := pemParse(data, "PRIVATE KEY")
	if err != nil {
		return nil, err
	}
	key, err := x509.ParsePKCS8PrivateKey(pemData)
	if err != nil {
		return nil, err
	}
	priKey, ok := key.(ed25519.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("Could not cast parsed key to ed25519.PrivateKey")
	}
	return priKey, nil
}

func parsePublicKey(data []byte) (ed25519.PublicKey, error) {
	pemData, err := pemParse(data, "PUBLIC KEY")
	if err != nil {
		return nil, err
	}
	key, err := x509.ParsePKIXPublicKey(pemData)
	if err != nil {
		return nil, err
	}
	pubKey, ok := key.(ed25519.PublicKey)
	if !ok {
		return nil, fmt.Errorf("Could not cast parsed key to ed25519.PublicKey")
	}
	return pubKey, nil
}

func pemParse(data []byte, pemType string) ([]byte, error) {
	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("No PEM block found")
	}
	if pemType != "" && block.Type != pemType {
		return nil, fmt.Errorf("key's type is '%s', expected '%s'", block.Type, pemType)
	}
	return block.Bytes, nil
}

2. 签名与校验

Sign 用私钥对消息签名,Verify 用公钥验签。注意这里的做法:不是直接对原始 msg 签名,而是先 hash256(SHA-256)得到 32 字节摘要,再让 ed25519 对摘要签名。所以验签时也必须先算同样的 SHA-256 摘要,再调 ed25519.Verify

package signer

import (
	"crypto"
	"crypto/ed25519"
	"encoding/hex"
	"fmt"

	"github.com/gofiber/fiber/v2/utils"
)

// Sign returns a signature of a message-hash.
// First the message is hashes (sha256) in order to shrink it,
// then the result hash is being signed with the private key.
func Sign(msg []byte, priKey string) (string, error) {
	key, err := parsePrivateKey(utils.UnsafeBytes(priKey))
	if err != nil {
		return "", fmt.Errorf("failed to parse private key: %w", err)
	}
	hashed := hash256(msg)
	signature := ed25519.Sign(key, hashed)
	return hex.EncodeToString(signature), nil
}

// Verify returns true if the given message and signature are valid, else false.
func Verify(msg []byte, sig string, pubKey string) bool {
	signature, err := hex.DecodeString(sig)
	if err != nil {
		return false
	}
	key, err := parsePublicKey(utils.UnsafeBytes(pubKey))
	if err != nil {
		return false
	}
	hashed := hash256(msg)
	return ed25519.Verify(key, hashed, signature)
}

func hash256(msg []byte) []byte {
	hash := crypto.SHA256.New()
	hash.Write(msg)
	return hash.Sum(nil)
}

3. 生成 PEM 密钥对

一段 main 演示怎么现场生成 ed25519 密钥,并分别序列化成 PKCS#8 私钥 / PKIX 公钥,再包成 PEM 打印出来:

package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"fmt"
)

func main() {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	pri, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		panic(err)
	}
	pub, err := x509.MarshalPKIXPublicKey(publicKey)
	if err != nil {
		panic(err)
	}
	fmt.Printf(
		"%s\n%s",
		pem.EncodeToMemory(&pem.Block{
			Type:  "PUBLIC KEY",
			Bytes: pub,
		}),
		pem.EncodeToMemory(&pem.Block{
			Type:  "PRIVATE KEY",
			Bytes: pri,
		}),
	)
}

注意事项

  • PEM 类型必须匹配:私钥是 PRIVATE KEY(PKCS#8),公钥是 PUBLIC KEY(PKIX / SubjectPublicKeyInfo)。pemParse 会强制校验 block.Type,不匹配直接报错,避免把公钥当私钥用。
  • 先哈希再签名是约定:ed25519 标准本身会对消息做内部哈希(SHA-512),这里额外先 SHA-256 是「先摘要再签名」的自定义约定——签名和验签两端必须都走 hash256,否则验不过。如果你希望直接对原始消息签名,把 ed25519.Sign(key, msg) / ed25519.Verify(key, msg, sig) 即可。
  • utils.UnsafeBytes 的依赖github.com/gofiber/fiber/v2/utilsUnsafeBytes 只是做 string→[]byte 的零拷贝转换。不想引入 gofiber 的话,直接写成 []byte(priKey) 也完全等价。
  • 解析失败是静默设计Verify 在 hex 解码失败或公钥解析失败时返回 false 而不返回 error,调用方拿不到原因;需要排查时建议先单独用 parsePublicKey 验证 PEM 内容。
  • 原示例里公钥类型断言失败的错误文案误写成了 PublickKey,这里已顺手改为 PublicKey