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/utils的UnsafeBytes只是做 string→[]byte 的零拷贝转换。不想引入 gofiber 的话,直接写成[]byte(priKey)也完全等价。- 解析失败是静默设计:
Verify在 hex 解码失败或公钥解析失败时返回false而不返回 error,调用方拿不到原因;需要排查时建议先单独用parsePublicKey验证 PEM 内容。 - 原示例里公钥类型断言失败的错误文案误写成了
PublickKey,这里已顺手改为PublicKey。