一.什么是ed25519
什么是ed25519?官网对ed25519签名有个很好的概括:
Ed25519: high-speed high-security signatures
Ed25519:一个高速度高安全的签名方法
有多快多安全呢?下面也给出了解释:
(以下内容摘自:Introduction)
Ed25519 is a public-key signature system with several attractive features:
- Fast single-signature verification.?The?software?takes only 273364 cycles to verify a signature on Intel's widely deployed Nehalem/Westmere lines of CPUs. (This performance measurement is for short messages; for very long messages, verification time is dominated by hashing time.) Nehalem and Westmere include all Core i7, i5, and i3 CPUs released between 2008 and 2010, and most Xeon CPUs released in the same period.
- Even faster batch verification.?The software performs a batch of 64 separate signature verifications (verifying 64 signatures of 64 messages under 64 public keys) in only 8.55 million cycles, i.e., under 134000 cycles per signature. The software fits easily into L1 cache, so contention between cores is negligible: a quad-core 2.4GHz Westmere verifies 71000 signatures per second, while keeping the maximum verification latency below 4 milliseconds.
- Very fast signing.?The software takes only 87548 cycles to sign a message. A quad-core 2.4GHz Westmere signs 109000 messages per second.
- Fast key generation.?Key generation is almost as fast as signing. There is a slight penalty for key generation to obtain a secure random number from the operating system;?/dev/urandom?under Linux costs about 6000 cycles.
- High security level.?This system has a 2^128 security target; breaking it has similar difficulty to breaking NIST P-256, RSA with ~3000-bit keys, strong 128-bit block ciphers, etc. The best attacks known actually cost more than 2^140 bit operations on average, and degrade quadratically in success probability as the number of bit operations drops.
- Foolproof session keys.?Signatures are generated deterministically; key generation consumes new randomness but new signatures do not. This is not only a speed feature but also a security feature, directly relevant to the recent collapse of the Sony PlayStation 3 security system.
- Collision resilience.?Hash-function collisions do not break this system. This adds a layer of defense against the possibility of weakness in the selected hash function.
- No secret array indices.?The software never reads or writes data from secret addresses in RAM; the pattern of addresses is completely predictable. The software is therefore immune to cache-timing attacks, hyperthreading attacks, and other side-channel attacks that rely on leakage of addresses through the CPU cache.
- No secret branch conditions.?The software never performs conditional branches based on secret data; the pattern of jumps is completely predictable. The software is therefore immune to side-channel attacks that rely on leakage of information through the branch-prediction unit.
- Small signatures.?Signatures fit into 64 bytes. These signatures are actually compressed versions of longer signatures; the times for compression and decompression are included in the cycle counts reported above.
- Small keys.?Public keys consume only 32 bytes. The times for compression and decompression are again included.
emmmmm,太长了,总结一下就是:ed25519是一个非对称加密的签名方法,它非常快、非常安全、产生的数据也非常小巧。
注意,上面的介绍中,最后两条单独拿出来说一下:它的签名长度为64个字节,公钥长度是32个字节,这两个数据后面会用到。
可能你会好奇,这个签名方法到底是有多安全?Google上有个问答对此进行了解答:
How secure is Ed25519?
Ed25519 is?considered to be secure?(similar difficulty to breaking a ~3000-bit RSA key). Creating a new signature with Ed25519 does not require a random input. This is very desirable from a security perspective . Ed25519 is resilient to hash-function collisions.
上条回答是2019年6月7日作出的,安全程度是“类似于破解一个~3000位RSA密钥的难度”。
所以ed25519非常适用于对于安全性有极高要求的系统,当然它也很快速和易用,如果您的系统对安全性要求不是很高,也可以考虑它来进行签名。
二.签名实现
签名一共经过三个环节
- 生成公钥、私钥
- 使用私钥签名
- 使用公钥验签
考虑到签名一般是用来校验跨端沟通是否安全有效,单独一个环境自嗨实在是没有必要,所以加上角色后,流程如下:
- A服务生成公钥、私钥,并将私钥提供给B服务端
- B服务获取私钥,用私钥将自己要签名的内容加签,将签名结果发送给A端
- A服务接到签名,用自己的公钥验签,验签成功,则签名有效
下面介绍一下各环境的实现,先以较为简单的单环境签名为例
js中的实现
注:我由于使用的vue,所以方法都管理在vue的methods模块,因此以下方法就没有加function关键词进行声明。
nodeJs与webjs实现同理,毕竟都是同一语言,本节就不做介绍,后面会介绍JS与NodeJS的跨端验签。
使用@noble/ed25519 进行签名
noble/ed25519是我用过的手感最好的包,入参和结果都非常贴心不用自己转码,小巧并且没有额外依赖,调用非常简单
首先要引入依赖
npm i?@noble/ed25519
import * as ed from '@noble/ed25519'
/**
* 使用 @noble/ed25519 进行签名
* signContent 签名内容
*/
async nobleSign(signContent) {
// 1. 生成私钥
let privateKey = ed.utils.randomPrivateKey()
// 2.生成公钥
let publicKey = await ed.getPublicKey(privateKey)
// 3.进行ed25519签名
let signature = await ed.sign(signContent, privateKey)
// 4.使用公钥进行验签,若结果为true,则此签名有效
let isSigned = await ed.verify(signature, signContent, publicKey)
console.log({
'私钥': privateKey,
'公钥': publicKey,
'签名': signature,
'验签成功': isSigned
})
}
传入签名内容进行调用即可
let msgHash = 'this is a demo'
nobleSign(msgHash)
运行结果
?js环境公私钥一般都是Uint8Array格式,要发给别端是要进行转码的,这点后面跨端沟通的时候会讲。
使用tweetnacl 进行签名
tweetnacl包是一个比较全面的前端加密库,里面包含ed25519加密签名
首先引入依赖
npm install tweetnacl
/**
* 使用tweetnacl进行签名
* signContent 签名内容
*/
tweetnaclSign(signContent) {
// 生成 公钥、私钥
let keyMap = nacl.sign.keyPair()
let publicKey = keyMap.publicKey
let secretKey = keyMap.secretKey
// 字符串转为Uint8Array格式,以供签名
let signArray = this.toUint8Arr(signContent)
// 进行签名
let signature = nacl.sign.detached(signArray, secretKey)
// 进行验签
let verify = nacl.sign.detached.verify(signArray, signature, publicKey)
console.log({
'公钥': publicKey,
'私钥': secretKey,
'签名': signature,
'验签成功': verify
})
}
// string转uint8array
toUint8Arr(str) {
const buffer = []
for (let i of str) {
const _code = i.charCodeAt(0)
if (_code < 0x80) {
buffer.push(_code)
} else if (_code < 0x800) {
buffer.push(0xc0 + (_code >> 6))
buffer.push(0x80 + (_code & 0x3f))
} else if (_code < 0x10000) {
buffer.push(0xe0 + (_code >> 12))
buffer.push(0x80 + (_code >> 6 & 0x3f))
buffer.push(0x80 + (_code & 0x3f))
}
}
return Uint8Array.from(buffer)
}
传入签名内容进行调用即可
let msgData = 'This is the second demo'
this.tweetnaclSign(msgData)
运行结果
?除了上面用到的几个api外,tweetnacl包还有几个好用的api
1.nacl.sign.keyPair.fromSecretKey(secretKey)
这个方法可以将Uint8Array格式的私钥转换为一对秘钥,从而获取公钥
2.nacl.sign(message, secretKey)
理论上来说,这个方法才是签名方法,但是我不知道为什么这个方法签名出来的结果长度是87位,不符合预期,因此选用了nacl.sign.detached(message, secretKey)方法进行签名。
下面是tweetnacl包关于ed25519签名的几个常量
nacl.sign.publicKeyLength = 32
nacl.sign.secretKeyLength = 64
nacl.sign.signatureLength = 64
公钥长度是32位,私钥长度是64位,签名长度是64位。
注意到和@noble/ed25519 包的区别了吗?@noble/ed25519包的私钥长度是32位而tweetnacl包要求私钥长度是64位!明明是同一个签名的实现,为什么私钥长度会不同呢?这点后面会再进行探究。
js还有很多包支持ed25519签名,比如js-nacl 也很好用,就不再一一列出了,有兴趣的研究者也可以去尝试一下。
java中的实现
先引入依赖包
maven形式:
<dependency>
<groupId>net.i2p.crypto</groupId>
<artifactId>eddsa</artifactId>
<version>0.3.0</version>
</dependency>
当然你要是用gradle:
compile("net.i2p.crypto:eddsa:0.3.0)
实现方法也非常简单,和js相比多了几步就是签名首先要用公钥、私钥初始化(init操作),之后再用签名数据去更新签名(update操作),最后再进行签名动作
import net.i2p.crypto.eddsa.*;
import net.i2p.crypto.eddsa.KeyPairGenerator;
import net.i2p.crypto.eddsa.spec.*;
import net.i2p.crypto.eddsa.EdDSAEngine;
/**
* 使用ed25519进行签名
* @return
*/
@RequestMapping("/ed25519Sign")
public Map ed25519Sign(){
// 签名内容
String signContent = "This is the third demo";
try {
// 生成秘钥对
KeyPairGenerator keyPairGenerator = new KeyPairGenerator();
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 使用ed25519签名
EdDSAEngine edDSAEngine = new EdDSAEngine();
edDSAEngine.initSign(keyPair.getPrivate());
edDSAEngine.update(signContent.getBytes());
byte[] signature = edDSAEngine.sign();
// 进行验签
edDSAEngine.initVerify(keyPair.getPublic());
edDSAEngine.update(signContent.getBytes());
Boolean verify = edDSAEngine.verify(signature);
System.out.println("公钥:" + Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
System.out.println("私钥:" + Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
System.out.println("签名结果:" + Base64.getEncoder().encodeToString(signature));
System.out.println("验签结果:" + verify);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
return null;
}
运行结果如下:
?这里为了方便控制台打印,对公私钥和签名结果都进行了base64转码。
go中的实现
go中就比较简单了,直接使用crypto库中的ed25519,不需要下载额外的依赖
import (
"crypto/ed25519"
"fmt"
)
/**
* 使用ed25519进行签名和验签
* msg 签名内容
*/
func ed25519Sign(msg string) {
// 生成公私钥
var publicKey []byte
var privateKey []byte
var err error
if publicKey, privateKey, err = ed25519.GenerateKey(nil); err != nil {
err = fmt.Errorf("decode private key fail: %s", err.Error())
return
}
msgByte := []byte(msg)
// 进行ed25519签名
signature := ed25519.Sign(privateKey, msgByte)
// 使用公钥进行验签
verify := ed25519.Verify(publicKey, msgByte, signature)
fmt.Println("公钥:", publicKey)
fmt.Println("私钥", privateKey)
fmt.Println("签名", signature)
fmt.Println("验签结果:", verify)
}
也是一样,调用一下即可
var msg = "This is the fourth demo"
ed25519Sign(msg)
运行结果如下:
?go里面公私钥和签名的格式都是切片 []byte,打印结果如上图。
小结:
至此,三种语言的ed25519签名都已实现,我们仔细看以上四种实现,不禁会有一个疑问:为什么同为ed25519,签名的结果差别都这么大?甚至同为js环境的加密包?@noble/ed25519 和?tweetnacl,签出来的结果格式都不同,这还怎么验证跨端签名?
其实,签名结果差别大,是应为各个语言支持的格式不一样,比如js中是uint8array格式,而在go中则成了切片[]byte 格式,虽然外在的表现格式不同,但是底层的签名实现是一致的,所以只要将签名数据转为各端都支持的格式(比如base64)进行传输,签名的时候再进行转码即可。
参考
Introduction?ed25519官网
ed25519 package - crypto/ed25519 - pkg.go.dev?go工具包 crypto/ed25519 介绍
draft-ietf-curdle-pkix-10
|