Учитывая возможность подписи из библиотеки Go NaCl (https://github.com/golang/crypto/tree/master/nacl/sign), как подписать файл, особенно очень большой файл размером более 1 ГБ? Большинство результатов поиска в Интернете посвящены подписи фрагмента или небольшого массива байтов.
Я могу думать о 2 способах:
Хороший пример austburn.me/blog/golang-server.html
Для файла, достаточно большого, чтобы вычисление одного хэша всего файла было нетривиальным, вы можете использовать хэши дерева Меркла, чтобы разделить файл на куски, а затем подписать корень хэша.
Из godoc: «Сообщения должны быть маленькими, потому что: 1. Все сообщение должно храниться в памяти для обработки. 2. Использование больших сообщений вынуждает реализации на небольших машинах обрабатывать открытый текст без проверки подписи. Это очень опасно, и этот API не одобряет это, но протокол, использующий чрезмерные размеры сообщений, может оставить некоторые реализации без другого выбора. 3. Производительность можно улучшить, работая с сообщениями, которые помещаются в кэши данных. Таким образом, большие объемы данных должны быть разбиты на части, чтобы каждое сообщение было небольшим».
@Натанаэль, спасибо. Можете ли вы преобразовать свой комментарий в ответ, пожалуйста? Это именно то, что я ищу. Я нашел нужные мне примеры: github.com/cbergoon/merkletree/blob/master/merkle_tree.goen.wikipedia.org/wiki/Merkle_tree. Я отмечу это как ответ.
Для подписи очень больших файлов (несколько гигабайт и выше) проблема использования стандартной функции подписи часто заключается во времени выполнения и хрупкости. Для очень больших файлов (или просто медленных дисков) может потребоваться несколько часов или больше, чтобы последовательно прочитать весь файл от начала до конца.
В таких случаях вам нужен способ параллельной обработки файла. Одним из распространенных способов сделать это, который подходит для криптографических подписей, являются хэши дерева Меркла. Они позволяют разбить большой файл на более мелкие фрагменты, хэшировать их параллельно (создавая «хэш-листы»), а затем дополнительно хешировать эти хэши в древовидной структуре для создания корневого хэша, представляющего полный файл.
После того, как вы вычислили этот корневой хэш дерева Меркла, вы можете подписать этот корневой хэш. Затем становится возможным использовать подписанный хэш корня дерева Меркла для параллельной проверки всех фрагментов файла, а также для проверки их порядка (на основе позиций хэшей листьев в древовидной структуре).
Проблема с NaCl в том, что вам нужно поместить все сообщение в ОЗУ, согласно годоку:
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
, который берет содержимое и вычисляет хеш-сумму — это наиболее эффективно.
Код ниже довольно сильно взломан, но вы должны получить представление. Я добился средней пропускной способности 315 МБ/с.
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
}
Спасибо за ответ. Действительно, сообщение должно быть в оперативной памяти. Алгоритм дерева Маркуса создаст единый корневой shasum, с которым я затем смогу подписать (меньше, меньше нагрузки на процессор из-за знака NaCL, и, что самое приятное, также облегчает потоковую передачу по сети). Я пойду с этим направлением.
@HollowayKeanHo Почему это должно быть в оперативной памяти? Вы можете скопировать любой io.Reader в хэш.
окончательный ввод (подпись shasum) в NaCL должен быть в ОЗУ для более быстрой обработки.
@HollowayKeanHo Прочтите код. Шасум находится в оперативной памяти.
Добро пожаловать в crypto.stackexchange. Кажется, это вопрос программирования, а вопросы программирования здесь не по теме, даже если они используют криптографию. Правильное место для вопросов по программированию — stackoverflow; Я могу перенести это туда для вас.