【问题标题】:How to use NaCl to sign a large file?如何使用 NaCl 对大文件进行签名?
【发布时间】:2019-04-07 01:02:28
【问题描述】:

鉴于 Go NaCl 库 (https://github.com/golang/crypto/tree/master/nacl/sign) 的签名能力,如何对文件进行签名,尤其是超过 1GB 的超大文件?大多数互联网搜索结果都是关于对切片或小字节数组进行签名的。

我能想到两种方法:

  1. 循环遍历文件并以块的方式流式传输(例如,每次 16k),然后将其输入到 sign 函数中。流式输出连接到签名证书中。为了验证,它是相反的。
  2. 使用 SHA(X) 生成文件的 shasum,然后对 shasum 输出进行签名。

【问题讨论】:

  • 欢迎来到 crypto.stackexchange - 这似乎是一个编程问题,即使他们使用密码学,编程问题在这里也是题外话。编程问题的正确位置在stackoverflow;我可以帮你迁移到那里。
  • 对于一个足够大的文件,计算整个文件的单个哈希并不简单,您可以使用 Merkle 树哈希将文件分成块,然后对哈希根进行签名。
  • 来自 godoc:“消息应该很小,因为: 1. 整个消息需要保存在内存中以进行处理。 2. 使用大型消息对小型机器上的实现施加压力,无需验证签名即可处理明文。这是非常危险的,这个 API 不鼓励这样做,但是使用过多消息大小的协议可能会呈现一些没有其他选择的实现。 3. 使用适合数据缓存的消息可以提高性能。因此,应该对大量数据进行分块,以使每条消息都很小。“。
  • @Natanael,谢谢。你能把你的评论变成答案吗?这正是我正在寻找的东西。我找到了我需要的示例:github.com/cbergoon/merkletree/blob/master/merkle_tree.goen.wikipedia.org/wiki/Merkle_tree。我会将其标记为答案。

标签: file go cryptography signature


【解决方案1】:

对于非常大的文件(数 GB 及以上)进行签名,使用标准签名函数的问题通常是运行时和脆弱性。对于非常大的文件(或只是慢速磁盘),从头到尾连续读取完整文件可能需要数小时或更长时间。

在这种情况下,您需要一种并行处理文件的方法。适用于加密签名的常用方法之一是 Merkle 树哈希。它们允许您将大文件分成更小的块,并行散列它们(产生“叶散列”),然后在树结构中进一步散列这些散列以生成代表完整文件的根散列。

一旦你计算了这个 Merkle 树根哈希,你就可以签署这个根哈希。然后可以使用签名的 Merkle 树根哈希来并行验证所有文件块,并验证它们的顺序(基于树结构中叶哈希的位置)。

【讨论】:

    【解决方案2】:

    NaCl 的问题是你需要将整个消息放入 RAM,as per godoc:

    消息应该很小,因为: 1. 整个消息需要保存在内存中以进行处理。 2. 使用大型消息对小型机器上的实现施加压力,无需验证签名即可处理明文。这是非常危险的,这个 API 不鼓励这样做,但是使用过多消息大小的协议可能会呈现一些没有其他选择的实现。 3. 使用适合数据缓存的消息可以提高性能。因此,应该对大量数据进行分块,以使每条消息都很小。

    但是,还有其他各种方法。他们中的大多数基本上都按照您在第一种方式中描述的方式进行。您基本上将文件内容复制到 io.Writer 中,该 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
    }
    

    【讨论】:

    • 感谢您的回答。实际上,消息必须在 RAM 中。 Markus 树算法将产生一个单一的根 shasum,然后我可以用它签名(更小,由于 NaCL 符号对 cpu 的压力更小,最好的部分,也有助于跨网络流式传输)。我会朝着这个方向前进。
    • @HollowayKeanHo 为什么它必须在 RAM 中?您可以将任何 io.Reader 复制到哈希中。
    • NaCL 的最终输入(shasum 签名)必须在 RAM 中以加快处理速度。
    • @HollowayKeanHo 阅读代码。 shasum 在 RAM 中。
    猜你喜欢
    • 1970-01-01
    • 2011-05-11
    • 2011-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    • 1970-01-01
    相关资源
    最近更新 更多