8 Go 密码学(五)非对称加密之椭圆曲线算法

一、椭圆曲线概述

椭圆曲线密码学(Elliptic curve cryptography),简称ECC,其和RAS类似属于公开秘钥的加密算法体系。ECC被公认为在给定密钥长度下最安全的加密算法。近年来由于比特币以太币等区块链应用ECC加密算法,业界也普遍看好ECC。

我们说过现代密码学是基于数学难题构建的,如RSA基于大数分解,ECC则基于椭圆曲线的椭圆曲线上的离散对数问题。

椭圆曲线是代数几何中一类重要的曲线,至今已有 100 多年的研究历史。而应用于密码学中的椭圆曲线是基于有限域上的,通过引入无穷远点,将椭圆曲线上的所有点和无穷远点组成一个集合,并在该集合上定义一个运算,从而该集合和运算构成了群。在有限域上的椭圆曲线群有两种,分别基于GF(p)以及GF(2m),它们各自有不同的群元素和群运算,然而对于群上的 ECDLP 问题,都认为是一个指数级的困难问题。基于这个困难问题,构建了ECC算法,包括==公钥加密、私钥解密、数字签名、签名验证、DH交换==等。

二、Go中使用ECDSA数字签名及签名验证

ECDSA 算法是目前使用最为广泛的标准算法,它是以 ECDLP 困难问题为基础,采用ELGamal体制构建的一个签名算法,它包含一个签名算法和一个验证算法。ECDSA算法如下:

首先选择好系统参数,如有限域类型和表示方法,曲线参数a,b,以及一个曲线上的基点G以及G的阶n,要求n必须为一个大素数(相关的系统参数的选择可见文献[23])。

参数确定以后,ECDSA 算法分为如下 3 个模块分别执行不同的功能,即密钥产生模块、数字签名模块以及签名验证模块。另外介绍一个基于ECC的Diffie-Hellman交换型算法以及公钥加密算法。

密钥产生:
1.在区间[1,n−1]上随机产生一个整数d(当然d不能太小)。
2.计算标量乘法Q=dG。
3.公开Q为公钥,保留d为私钥。

数字签名:
1.使用安全散列函数H对需要签名的消息M进行杂凑计算e=H(M)。
2.随机生成一个区间[1,n−1]上的本地秘密随机数k,并计算kG=(x1,y1)。
3.计算r=x1 mod n。
4.计算s=k−1(e+dr)mod n。
5.则数据r||s即为ECDSA算法下对消息M的签名(其中||表示两个比特串的串接)。

签名验证:
1.使用与签名一样的散列算法H计算e=H(M)。
2.计算c=s−1 mod n。
3.计算u1=ec mod n,u2=rc mod n。
4.计算(x1,y1)=u1G+uQ2。
5.计算v=x1mod n。若v=r,则为一个合法签名,否则验证不通过。

DH交换:
1.A随机生成一个区间[1,n−1]上的本地秘密随机数k,计算并发送kG到B。
2.B随机生成一个区间[1,n−1]上的本地秘密随机数l,计算并发送lG到A。
3.最后A计算k(lG)同时B计算l(kG)作为双方的共享密钥。

公钥加密算法:
1.公钥加密:随机生成一个区间[1,n-1]上的本地秘密随机数k,并计算,对需要加密的消息M。计算的密文C=kG||kQ+M(其中+为XOR运算)。
2.私钥解密:M=(kQ+M)−d(kG)。
1.生成密钥对
import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "time"

    "crypto/x509"
    "encoding/pem"
    "errors"
    mathRand "math/rand"
    "os"
    "strings"
)

const (
    PRIVATEFILE = "src/cryptography/myECDSA/privateKey.pem"
    PUBLICFILE  = "src/cryptography/myECDSA/publicKey.pem"
)

//生成指定math/rand字节长度的随机字符串
func GetRandomString(length int) string {
    str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+?=-"
    bytes := []byte(str)
    result := []byte{}

    r := mathRand.New(mathRand.NewSource(time.Now().UnixNano()))
    for i := 0; i < length; i++ {
        result = append(result, bytes[r.Intn(len(bytes))])
    }
    return string(result)
}

//生成ECC算法的公钥和私钥文件
//根据随机字符串生成,randKey至少36位
func GenerateKey(randKey string) error {

    var err error
    var privateKey *ecdsa.PrivateKey
    var publicKey ecdsa.PublicKey
    var curve elliptic.Curve

    //一、生成私钥文件

    //根据随机字符串长度设置curve曲线
    length := len(randKey)
    //elliptic包实现了几条覆盖素数有限域的标准椭圆曲线,Curve代表一个短格式的Weierstrass椭圆曲线,其中a=-3
    if length < 224/8 {
        err = errors.New("私钥长度太短,至少为36位!")
        return err
    }

    if length >= 521/8+8 {
        //长度大于73字节,返回一个实现了P-512的曲线
        curve = elliptic.P521()
    } else if length >= 384/8+8 {
        //长度大于56字节,返回一个实现了P-384的曲线
        curve = elliptic.P384()
    } else if length >= 256/8+8 {
        //长度大于40字节,返回一个实现了P-256的曲线
        curve = elliptic.P256()
    } else if length >= 224/8+8 {
        //长度大于36字节,返回一个实现了P-224的曲线
        curve = elliptic.P224()
    }

    //GenerateKey方法生成私钥
    privateKey, err = ecdsa.GenerateKey(curve, strings.NewReader(randKey))
    if err != nil {
        return err
    }
    //通过x509标准将得到的ecc私钥序列化为ASN.1的DER编码字符串
    privateBytes, err := x509.MarshalECPrivateKey(privateKey)
    if err != nil {
        return err
    }
    //将私钥字符串设置到pem格式块中
    privateBlock := pem.Block{
        Type:  "ecc private key",
        Bytes: privateBytes,
    }

    //通过pem将设置好的数据进行编码,并写入磁盘文件
    privateFile, err := os.Create(PRIVATEFILE)
    if err != nil {
        return err
    }
    defer privateFile.Close()
    err = pem.Encode(privateFile, &privateBlock)
    if err != nil {
        return err
    }

    //二、生成公钥文件
    //从得到的私钥对象中将公钥信息取出
    publicKey = privateKey.PublicKey

    //通过x509标准将得到的ecc公钥序列化为ASN.1的DER编码字符串
    publicBytes, err := x509.MarshalPKIXPublicKey(&publicKey)
    if err != nil {
        return err
    }
    //将公钥字符串设置到pem格式块中
    publicBlock := pem.Block{
        Type:  "ecc public key",
        Bytes: publicBytes,
    }

    //通过pem将设置好的数据进行编码,并写入磁盘文件
    publicFile, err := os.Create(PUBLICFILE)
    if err != nil {
        return err
    }
    err = pem.Encode(publicFile, &publicBlock)
    if err != nil {
        return err
    }

    return nil
}
2.ECDSA 签名及校验
import (
    "bytes"
    "compress/gzip"
    "crypto/ecdsa"
    "encoding/hex"
    "errors"
    "math/big"
    "strings"
)

//使用ECC算法加密签名,返回签名数据
func CryptSignByEcc(input, priKeyFile, randSign string) (output string, err error) {
    //获取私钥
    privateKey, err := GetPrivateKeyByPemFile(priKeyFile)
    if err != nil {
        return "", err
    }

    //ecc私钥和随机签字符串数据得到哈希
    r, s, err := ecdsa.Sign(strings.NewReader(randSign), privateKey, []byte(input))
    if err != nil {
        return "", err
    }

    rt, err := r.MarshalText()
    if err != nil {
        return "", err
    }

    st, err := s.MarshalText()
    if err != nil {
        return "", err
    }

    //拼接两个椭圆曲线参数哈希
    var b bytes.Buffer
    writer := gzip.NewWriter(&b)
    defer writer.Close()

    _, err = writer.Write([]byte(string(rt) + "+" + string(st)))
    if err != nil {
        return "", err
    }
    writer.Flush()

    return hex.EncodeToString(b.Bytes()), nil
}

//使用ECC算法,对密文和明文进行匹配校验
func VerifyCryptEcc(srcStr, cryptStr string) (bool, error) {

    decodeBytes, err := hex.DecodeString(cryptStr)
    if err != nil {
        return false, err
    }

    //解密签名信息,返回椭圆曲线参数:两个大整数
    rint, sint, err := UnSignCryptEcc(decodeBytes)

    //获取公钥验证数据
    publicKey, err := GetPublicKeyByPemFile(PUBLICFILE)
    if err != nil {
        return false, err
    }
    //使用公钥、原文、以及签名信息解密后的两个椭圆曲线的大整数参数进行校验
    verify := ecdsa.Verify(publicKey, []byte(srcStr), &rint, &sint)

    return verify, nil
}

//使用ECC算法解密,返回加密前的椭圆曲线大整数
func UnSignCryptEcc(cryptBytes []byte) (rint, sint big.Int, err error) {
    reader, err := gzip.NewReader(bytes.NewBuffer(cryptBytes))
    if err != nil {
        err = errors.New("decode error," + err.Error())
    }
    defer reader.Close()

    buf := make([]byte, 1024)
    count, err := reader.Read(buf)
    if err != nil {
        err = errors.New("decode read error," + err.Error())
    }

    rs := strings.Split(string(buf[:count]), "+")
    if len(rs) != 2 {
        err = errors.New("decode fail")
        return
    }
    err = rint.UnmarshalText([]byte(rs[0]))
    if err != nil {
        err = errors.New("decrypt rint fail, " + err.Error())
        return
    }
    err = sint.UnmarshalText([]byte(rs[1]))
    if err != nil {
        err = errors.New("decrypt sint fail, " + err.Error())
        return
    }
    return
}
获取私钥文件里的数据:
func GetPrivateKeyByPemFile(priKeyFile string) (*ecdsa.PrivateKey, error) {
    //将私钥文件中的私钥读出,得到使用pem编码的字符串
    file, err := os.Open(priKeyFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    fileInfo, err := file.Stat()
    if err != nil {
        return nil, err
    }
    size := fileInfo.Size()
    buffer := make([]byte, size)
    _, err = file.Read(buffer)
    if err != nil {
        return nil, err
    }
    //将得到的字符串解码
    block, _ := pem.Decode(buffer)

    //使用x509将编码之后的私钥解析出来
    privateKey, err := x509.ParseECPrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    return privateKey, nil
}
获取公钥文件里的数据:
func GetPublicKeyByPemFile(pubKeyFile string) (*ecdsa.PublicKey, error) {
    var err error
    //从公钥文件获取钥匙字符串
    file, err := os.Open(pubKeyFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    fileInfo, err := file.Stat()
    if err != nil {
        return nil, err
    }

    buffer := make([]byte, fileInfo.Size())
    _, err = file.Read(buffer)
    if err != nil {
        return nil, err
    }
    //将得到的字符串解码
    block, _ := pem.Decode(buffer)

    //使用x509将编码之后的公钥解析出来
    pubInner, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    publicKey := pubInner.(*ecdsa.PublicKey)

    return publicKey, nil
}
数字签名及演示
func TestECDSA() {
    //生成随机钥字符串长度40字节,用于生产公私钥证书
    randKey := myECDSA.GetRandomString(40)
    //生成随机签名字符串40字节,用于加密数据
    randSign := myECDSA.GetRandomString(40)

    //使用随机钥字符串生成公私钥文件
    e := myECDSA.GenerateKey(randKey)
    if e != nil {
        fmt.Println(e)
    }

    //签名附加信息
    srcInfo := "GO 密码学 —— ECDSA 椭圆曲线实现数字签名"
    fmt.Println("原文:", srcInfo)

    //ECC签名加密
    signByEcc, e := myECDSA.CryptSignByEcc(srcInfo, myECDSA.PRIVATEFILE, randSign)
    if e != nil {
        fmt.Println(e)
    }
    fmt.Println("ECDSA私钥加密签名为:", signByEcc)

    //ECC签名算法校验
    verifyCryptEcc, e := myECDSA.VerifyCryptEcc(srcInfo, signByEcc)
    if e != nil {
        fmt.Println(e)
    }
    fmt.Println("ECDSA公钥解密后验签校验结果:", verifyCryptEcc)

}

//OUTPUT:
原文: GO 密码学 —— ECDSA 椭圆曲线实现数字签名
ECDSA私钥加密签名为: 1f8b08000000000000ff14cbb10d43510c02c081d2609e31b0ff62d16f4fbac1bd3a8666cb598f9f595e208fa950d9e4721457071f85ae9a06d1e37b600dc0f885fa0c37aea7678ac6ce75c4d4e5c386ad446a0fd2adced767ccf70628ac81f60f0000ffff
ECDSA公钥解密后验签校验结果: true

三、GO 使用ECIES 加解密算法

go标准包的ECDSA仅支持数字签名和验签,对数据传输的加解密还未提供,不过以太坊基于crypto/ecdsa实现了ECIES加解密算法,github.com/ethereum/go-ethereum ,感兴趣的可阅读器源码使用,该包声明未经过审核,以下演示仅供个人学习使用:

1.生成ECIES密钥对
package myECIES

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "github.com/ethereum/go-ethereum/crypto/ecies"
    "strings"
    "time"

    "crypto/x509"
    "encoding/pem"
    "errors"
    mathRand "math/rand"
    "os"
)

const (
    PRIVATEFILE = "src/cryptography/myECIES/privateKey.pem"
    PUBLICFILE  = "src/cryptography/myECIES/publicKey.pem"
)

//生成指定math/rand字节长度的随机字符串
func GetRandomString(length int) string {
    str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+?=-"
    bytes := []byte(str)
    result := []byte{}

    r := mathRand.New(mathRand.NewSource(time.Now().UnixNano()))
    for i := 0; i < length; i++ {
        result = append(result, bytes[r.Intn(len(bytes))])
    }
    return string(result)
}

//生成ECC算法的公钥和私钥文件
//根据随机字符串生成,randKey至少36位
func GenerateKey(randKey string) error {

    var err error
    var privateKey *ecdsa.PrivateKey
    var publicKey ecdsa.PublicKey
    var curve elliptic.Curve

    //一、生成私钥文件

    //根据随机字符串长度设置curve曲线
    length := len(randKey)
    //elliptic包实现了几条覆盖素数有限域的标准椭圆曲线,Curve代表一个短格式的Weierstrass椭圆曲线,其中a=-3
    if length < 224/8 {
        err = errors.New("私钥长度太短,至少为36位!")
        return err
    }

    if length >= 521/8+8 {
        //长度大于73字节,返回一个实现了P-512的曲线
        curve = elliptic.P521()
    } else if length >= 384/8+8 {
        //长度大于56字节,返回一个实现了P-384的曲线
        curve = elliptic.P384()
    } else if length >= 256/8+8 {
        //长度大于40字节,返回一个实现了P-256的曲线
        curve = elliptic.P256()
    } else if length >= 224/8+8 {
        //长度大于36字节,返回一个实现了P-224的曲线
        curve = elliptic.P224()
    }

    //GenerateKey方法生成私钥
    privateKey, err = ecdsa.GenerateKey(curve, strings.NewReader(randKey))
    if err != nil {
        return err
    }
    //通过x509标准将得到的ecc私钥序列化为ASN.1的DER编码字符串
    privateBytes, err := x509.MarshalECPrivateKey(privateKey)
    if err != nil {
        return err
    }
    //将私钥字符串设置到pem格式块中
    privateBlock := pem.Block{
        Type:  "ecc private key",
        Bytes: privateBytes,
    }

    //通过pem将设置好的数据进行编码,并写入磁盘文件
    privateFile, err := os.Create(PRIVATEFILE)
    if err != nil {
        return err
    }
    defer privateFile.Close()
    err = pem.Encode(privateFile, &privateBlock)
    if err != nil {
        return err
    }

    //二、生成公钥文件
    //从得到的私钥对象中将公钥信息取出
    publicKey = privateKey.PublicKey

    //通过x509标准将得到的ecc公钥序列化为ASN.1的DER编码字符串
    publicBytes, err := x509.MarshalPKIXPublicKey(&publicKey)
    if err != nil {
        return err
    }
    //将公钥字符串设置到pem格式块中
    publicBlock := pem.Block{
        Type:  "ecc public key",
        Bytes: publicBytes,
    }

    //通过pem将设置好的数据进行编码,并写入磁盘文件
    publicFile, err := os.Create(PUBLICFILE)
    if err != nil {
        return err
    }
    err = pem.Encode(publicFile, &publicBlock)
    if err != nil {
        return err
    }

    return nil
}

//获取私钥文件里的私钥内容函数
func GetPrivateKeyByPemFile(priKeyFile string) (*ecies.PrivateKey, error) {
    //将私钥文件中的私钥读出,得到使用pem编码的字符串
    file, err := os.Open(priKeyFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    fileInfo, err := file.Stat()
    if err != nil {
        return nil, err
    }
    size := fileInfo.Size()
    buffer := make([]byte, size)
    _, err = file.Read(buffer)
    if err != nil {
        return nil, err
    }
    //将得到的字符串解码
    block, _ := pem.Decode(buffer)

    //使用x509将编码之后的私钥解析出来
    privateKey, err := x509.ParseECPrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    //读取文件的ecdsa私钥转化成ecies私钥
    privateKeyForEcies := ecies.ImportECDSA(privateKey)

    return privateKeyForEcies, nil
}

//获取公钥文件里的公钥内容函数
func GetPublicKeyByPemFile(pubKeyFile string) (*ecies.PublicKey, error) {
    var err error
    //从公钥文件获取钥匙字符串
    file, err := os.Open(pubKeyFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    fileInfo, err := file.Stat()
    if err != nil {
        return nil, err
    }

    buffer := make([]byte, fileInfo.Size())
    _, err = file.Read(buffer)
    if err != nil {
        return nil, err
    }
    //将得到的字符串解码
    block, _ := pem.Decode(buffer)

    //使用x509将编码之后的公钥解析出来
    pubInner, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    publicKey := pubInner.(*ecdsa.PublicKey)

    publicKeyForEcies := ecies.ImportECDSAPublic(publicKey)

    return publicKeyForEcies, nil
}
2.数据加解密函数的实现:
import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "github.com/ethereum/go-ethereum/crypto/ecies"
)

//ECIES 公钥数据加密
func EnCryptByEcies(srcData, publicFile string) (cryptData string, err error) {
    //获取公钥数据
    publicKey, err := GetPublicKeyByPemFile(publicFile)
    if err != nil {
        return "", err
    }

    //公钥加密数据
    encryptBytes, err := ecies.Encrypt(rand.Reader, publicKey, []byte(srcData), nil, nil)
    if err != nil {
        return "", err
    }

    cryptData = hex.EncodeToString(encryptBytes)

    return
}

//ECIES 私钥数据解密
func DeCryptByEcies(cryptData, privateFile string) (srcData string, err error) {
    //获取私钥信息
    privateKey, err := GetPrivateKeyByPemFile(privateFile)
    if err != nil {
        return "", err
    }

    //私钥解密数据
    cryptBytes, err := hex.DecodeString(cryptData)
    srcByte, err := privateKey.Decrypt(cryptBytes, nil, nil)
    if err != nil {
        fmt.Println("解密错误:", err)
        return "", err
    }
    srcData = string(srcByte)

    return
}
3.加解密演示:
//测试ECC椭圆曲线实现数据加解密
func TestECIES() {
    //获取随机字符串
    randomKey := myECIES.GetRandomString(40)

    //生成私钥和公钥
    e := myECIES.GenerateKey(randomKey)
    if e != nil {
        fmt.Println(e)
    }

    //加密前源信息
    srcInfo := "GO 密码学 —— ECIES 椭圆曲线实现数据加解密"
    fmt.Println("原文:", srcInfo)

    //加密信息
    cryptData, e := myECIES.EnCryptByEcies(srcInfo, myECIES.PUBLICFILE)
    if e != nil {
        fmt.Println(e)
    }
    fmt.Println("ECIES加密后为:", cryptData)

    //解密信息
    srcData, e := myECIES.DeCryptByEcies(cryptData, myECIES.PRIVATEFILE)
    if e != nil {
        fmt.Println(e)
    }
    fmt.Println("ECIES解密后为:", srcData)

}

//OUTPUT:
原文: GO 密码学 —— ECIES 椭圆曲线实现数据加解密
ECIES加密后为: 0494ba1ad5e9d4606e8360432723727a10fac1206f64063414dd038df359ccb663725a3cd4a17a07330e1ec52f6a40a7ee278ea7491f9c0beace8ef283152555bd86da3f622408503266e1dcadb0efd1d371cedbd874f0b08b3f9a3dbd47da4cf1917e4c0d20d913dbe8851db38895de46377bfd929b4432a07c8d99da89127da5a8a212b400aed12bdb172a8b219d847106bad4f2ea21ac4d758447bc70f798b5d4272088e435ab3804337d
ECIES解密后为: GO 密码学 —— ECIES 椭圆曲线实现数据加解密

至此Go 密码学实践告一段落,其具体实战应该后期会在实战专题展开。

你可能感兴趣的:(算法,密码学,python,加密解密,java)