如何使用 NaCl 对大文件进行签名?
How to use NaCl to sign a large file?
鉴于Go NaCl 库(https://github.com/golang/crypto/tree/master/nacl/sign) 的签名功能,如何对文件进行签名,尤其是超过1GB 的超大文件?大多数 Internet 搜索结果都是关于对切片或小字节数组进行签名的。
我可以想到2个方法:
- 循环遍历文件并以块方式流式传输(例如每次 16k),然后将其输入符号函数。流式输出连接成签名证书。为了验证,反着做。
- 使用 SHA(X) 生成文件的 shasum,然后对 shasum 输出进行签名。
对于非常大的文件(数 GB 及以上)进行签名,使用标准签名功能的问题通常是运行时和脆弱性。对于非常大的文件(或只是慢速磁盘),从头到尾连续读取整个文件可能需要数小时或更长时间。
在这种情况下,您需要一种并行处理文件的方法。一种适用于加密签名的常见方法是 Merkle 树哈希。它们允许您将大文件拆分为较小的块,并行散列它们(生成 "leaf hashes"),然后进一步散列树结构中的这些散列以生成代表完整文件的根散列。
一旦你计算出这个 Merkle 树根哈希,你就可以签署这个根哈希。然后可以使用签名的 Merkle 树根哈希并行验证所有文件块,以及验证它们的顺序(基于树结构中叶哈希的位置)。
NaCl 的问题是您需要将整个消息放入 RAM,as per godoc:
Messages should be small because: 1. The whole message needs to be held in memory to be processed. 2. Using large messages pressures implementations on small machines to process plaintext without verifying the signature. This is very dangerous, and this API discourages it, but a protocol that uses excessive message sizes might present some implementations with no other choice. 3. Performance may be improved by working with messages that fit into data caches. Thus large amounts of data should be chunked so that each message is small.
但是,还有其他各种方法。他们中的大多数人基本上按照您在第一种方式中描述的方式进行操作。您基本上将文件内容复制到一个 io.Writer
中,它获取内容并计算哈希和 - 这是最有效的。
下面的代码很乱,但你应该明白了。
我用它实现了 315MB/s 的平均吞吐量。
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"flag"
"fmt"
"io"
"log"
"math/big"
"os"
"time"
)
var filename = flag.String("file", "", "file to sign")
func main() {
flag.Parse()
if *filename == "" {
log.Fatal("file can not be empty")
}
f, err := os.Open(*filename)
if err != nil {
log.Fatalf("Error opening '%s': %s", *filename, err)
}
defer f.Close()
start := time.Now()
sum, n, err := hash(f)
duration := time.Now().Sub(start)
log.Printf("Hashed %s (%d bytes)in %s to %x", *filename, n, duration, sum)
log.Printf("Average: %.2f MB/s", (float64(n)/1000000)/duration.Seconds())
r, s, err := sign(sum)
if err != nil {
log.Fatalf("Error creatig signature: %s", err)
}
log.Printf("Signature: (0x%x,0x%x)\n", r, s)
}
func sign(sum []byte) (*big.Int, *big.Int, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Printf("Error creating private key: %s", err)
}
return ecdsa.Sign(rand.Reader, priv, sum[:])
}
func hash(f *os.File) ([]byte, int64, error) {
var (
hash []byte
n int64
err error
)
h := sha256.New()
// This is where the magic happens.
// We use the efficient io.Copy to feed the contents
// of the file into the hash function.
if n, err = io.Copy(h, f); err != nil {
return nil, n, fmt.Errorf("Error creating hash: %s", err)
}
hash = h.Sum(nil)
return hash, n, nil
}
鉴于Go NaCl 库(https://github.com/golang/crypto/tree/master/nacl/sign) 的签名功能,如何对文件进行签名,尤其是超过1GB 的超大文件?大多数 Internet 搜索结果都是关于对切片或小字节数组进行签名的。
我可以想到2个方法:
- 循环遍历文件并以块方式流式传输(例如每次 16k),然后将其输入符号函数。流式输出连接成签名证书。为了验证,反着做。
- 使用 SHA(X) 生成文件的 shasum,然后对 shasum 输出进行签名。
对于非常大的文件(数 GB 及以上)进行签名,使用标准签名功能的问题通常是运行时和脆弱性。对于非常大的文件(或只是慢速磁盘),从头到尾连续读取整个文件可能需要数小时或更长时间。
在这种情况下,您需要一种并行处理文件的方法。一种适用于加密签名的常见方法是 Merkle 树哈希。它们允许您将大文件拆分为较小的块,并行散列它们(生成 "leaf hashes"),然后进一步散列树结构中的这些散列以生成代表完整文件的根散列。
一旦你计算出这个 Merkle 树根哈希,你就可以签署这个根哈希。然后可以使用签名的 Merkle 树根哈希并行验证所有文件块,以及验证它们的顺序(基于树结构中叶哈希的位置)。
NaCl 的问题是您需要将整个消息放入 RAM,as per godoc:
Messages should be small because: 1. The whole message needs to be held in memory to be processed. 2. Using large messages pressures implementations on small machines to process plaintext without verifying the signature. This is very dangerous, and this API discourages it, but a protocol that uses excessive message sizes might present some implementations with no other choice. 3. Performance may be improved by working with messages that fit into data caches. Thus large amounts of data should be chunked so that each message is small.
但是,还有其他各种方法。他们中的大多数人基本上按照您在第一种方式中描述的方式进行操作。您基本上将文件内容复制到一个 io.Writer
中,它获取内容并计算哈希和 - 这是最有效的。
下面的代码很乱,但你应该明白了。 我用它实现了 315MB/s 的平均吞吐量。
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"flag"
"fmt"
"io"
"log"
"math/big"
"os"
"time"
)
var filename = flag.String("file", "", "file to sign")
func main() {
flag.Parse()
if *filename == "" {
log.Fatal("file can not be empty")
}
f, err := os.Open(*filename)
if err != nil {
log.Fatalf("Error opening '%s': %s", *filename, err)
}
defer f.Close()
start := time.Now()
sum, n, err := hash(f)
duration := time.Now().Sub(start)
log.Printf("Hashed %s (%d bytes)in %s to %x", *filename, n, duration, sum)
log.Printf("Average: %.2f MB/s", (float64(n)/1000000)/duration.Seconds())
r, s, err := sign(sum)
if err != nil {
log.Fatalf("Error creatig signature: %s", err)
}
log.Printf("Signature: (0x%x,0x%x)\n", r, s)
}
func sign(sum []byte) (*big.Int, *big.Int, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Printf("Error creating private key: %s", err)
}
return ecdsa.Sign(rand.Reader, priv, sum[:])
}
func hash(f *os.File) ([]byte, int64, error) {
var (
hash []byte
n int64
err error
)
h := sha256.New()
// This is where the magic happens.
// We use the efficient io.Copy to feed the contents
// of the file into the hash function.
if n, err = io.Copy(h, f); err != nil {
return nil, n, fmt.Errorf("Error creating hash: %s", err)
}
hash = h.Sum(nil)
return hash, n, nil
}