// Copyright (c) 2021 Andreas Auernhammer. All rights reserved. // Use of this source code is governed by a license that can be // found in the LICENSE file. // Package minisign implements the minisign signature scheme. package minisign import ( "bytes" "crypto/ed25519" "encoding/binary" "hash" "io" "strconv" "strings" "time" "golang.org/x/crypto/blake2b" ) const ( // EdDSA refers to the Ed25519 signature scheme. // // Minisign uses this signature scheme to sign and // verify (non-hashed) messages. EdDSA uint16 = 0x6445 // HashEdDSA refers to a Ed25519 signature scheme // with pre-hashed messages. // // Minisign uses this signature scheme to sign and // verify message that don't fit into memory. HashEdDSA uint16 = 0x4445 ) // GenerateKey generates a public/private key pair using entropy // from random. If random is nil, crypto/rand.Reader will be used. func GenerateKey(random io.Reader) (PublicKey, PrivateKey, error) { pub, priv, err := ed25519.GenerateKey(random) if err != nil { return PublicKey{}, PrivateKey{}, err } id := blake2b.Sum256(pub[:]) publicKey := PublicKey{ id: binary.LittleEndian.Uint64(id[:8]), } copy(publicKey.bytes[:], pub) privateKey := PrivateKey{ id: publicKey.ID(), } copy(privateKey.bytes[:], priv) return publicKey, privateKey, nil } // Reader is an io.Reader that reads a message // while, at the same time, computes its digest. // // At any point, typically at the end of the message, // Reader can sign the message digest with a private // key or try to verify the message with a public key // and signature. type Reader struct { message io.Reader hash hash.Hash } // NewReader returns a new Reader that reads from r // and computes a digest of the read data. func NewReader(r io.Reader) *Reader { h, err := blake2b.New512(nil) if err != nil { panic(err) } return &Reader{ message: r, hash: h, } } // Read reads from the underlying io.Reader as specified // by the io.Reader interface. func (r *Reader) Read(p []byte) (int, error) { n, err := r.message.Read(p) r.hash.Write(p[:n]) return n, err } // Sign signs whatever has been read from the underlying // io.Reader up to this point in time with the given private // key. // // It behaves like SignWithComments but uses some generic comments. func (r *Reader) Sign(privateKey PrivateKey) []byte { var ( trustedComment = "timestamp:" + strconv.FormatInt(time.Now().Unix(), 10) untrustedComment = "signature from private key: " + strings.ToUpper(strconv.FormatUint(privateKey.ID(), 16)) ) return r.SignWithComments(privateKey, trustedComment, untrustedComment) } // SignWithComments signs whatever has been read from the underlying // io.Reader up to this point in time with the given private key. // // The trustedComment as well as the untrustedComment are embedded into the // returned signature. The trustedComment is signed and will be checked // when the signature is verified. The untrustedComment is not signed and // must not be trusted. // // SignWithComments computes the digest as a snapshot. So, it is possible // to create multiple signatures of different message prefixes by reading // up to a certain byte, signing this message prefix, and then continue // reading. func (r *Reader) SignWithComments(privateKey PrivateKey, trustedComment, untrustedComment string) []byte { const isHashed = true return sign(privateKey, r.hash.Sum(nil), trustedComment, untrustedComment, isHashed) } // Verify checks whether whatever has been read from the underlying // io.Reader up to this point in time is authentic by verifying it // with the given public key and signature. // // Verify computes the digest as a snapshot. Therefore, Verify can // verify any signature produced by Sign or SignWithComments, // including signatures of partial messages, given the correct // public key and signature. func (r *Reader) Verify(publicKey PublicKey, signature []byte) bool { const isHashed = true return verify(publicKey, r.hash.Sum(nil), signature, isHashed) } // Sign signs the given message with the private key. // // It behaves like SignWithComments with some generic comments. func Sign(privateKey PrivateKey, message []byte) []byte { var ( trustedComment = "timestamp:" + strconv.FormatInt(time.Now().Unix(), 10) untrustedComment = "signature from private key: " + strings.ToUpper(strconv.FormatUint(privateKey.ID(), 16)) ) return SignWithComments(privateKey, message, trustedComment, untrustedComment) } // SignWithComments signs the given message with the private key. // // The trustedComment as well as the untrustedComment are embedded // into the returned signature. The trustedComment is signed and // will be checked when the signature is verified. // The untrustedComment is not signed and must not be trusted. func SignWithComments(privateKey PrivateKey, message []byte, trustedComment, untrustedComment string) []byte { const isHashed = false return sign(privateKey, message, trustedComment, untrustedComment, isHashed) } // Verify checks whether message is authentic by verifying // it with the given public key and signature. It returns // true if and only if the signature verification is successful. func Verify(publicKey PublicKey, message, signature []byte) bool { const isHashed = false return verify(publicKey, message, signature, isHashed) } func sign(privateKey PrivateKey, message []byte, trustedComment, untrustedComment string, isHashed bool) []byte { var algorithm = EdDSA if isHashed { algorithm = HashEdDSA } var ( msgSignature = ed25519.Sign(ed25519.PrivateKey(privateKey.bytes[:]), message) commentSignature = ed25519.Sign(ed25519.PrivateKey(privateKey.bytes[:]), append(msgSignature, []byte(trustedComment)...)) ) signature := Signature{ Algorithm: algorithm, KeyID: privateKey.ID(), TrustedComment: trustedComment, UntrustedComment: untrustedComment, } copy(signature.Signature[:], msgSignature) copy(signature.CommentSignature[:], commentSignature) text, err := signature.MarshalText() if err != nil { panic(err) } return text } func verify(publicKey PublicKey, message, signature []byte, isHashed bool) bool { var s Signature if err := s.UnmarshalText(signature); err != nil { return false } if s.KeyID != publicKey.ID() { return false } if s.Algorithm == HashEdDSA && !isHashed { h := blake2b.Sum512(message) message = h[:] } if !ed25519.Verify(ed25519.PublicKey(publicKey.bytes[:]), message, s.Signature[:]) { return false } globalMessage := append(s.Signature[:], []byte(s.TrustedComment)...) return ed25519.Verify(ed25519.PublicKey(publicKey.bytes[:]), globalMessage, s.CommentSignature[:]) } // trimUntrustedComment returns text with a potential // untrusted comment line. func trimUntrustedComment(text []byte) []byte { s := bytes.SplitN(text, []byte{'\n'}, 2) if len(s) == 2 && strings.HasPrefix(string(s[0]), "untrusted comment: ") { return s[1] } return s[0] }