From 4ade9f8387117bd5be0a8301ce5c5bdb327a76b8 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Tue, 31 Oct 2023 12:28:34 +0100 Subject: [PATCH 1/9] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ee7ec6..afd1ca3 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,11 @@ Options: On windows, linux and macOS, you can use the pre-built binaries: | OS | ARCH | Latest Release | |:---------:|:-------:|:-----------------------------------------------------------------------------------------------------------------------| -| Linux | amd64 | [minisign-linux-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.0/minisign-linux-amd64.tar.gz) | -| Linux | arm64 | [minisign-linux-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.0/minisign-linux-arm64.tar.gz) | -| MacOS | arm64 | [minisign-darwin-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.0/minisign-darwin-arm64.tar.gz) | -| MacOS | amd64 | [minisign-darwin-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.0/minisign-darwin-amd64.tar.gz) | -| Windows | amd64 | [minisign-windows-amd64.zip](https://github.com/aead/minisign/releases/download/v0.2.0/minisign-windows-amd64.zip) | +| Linux | amd64 | [minisign-linux-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-linux-amd64.tar.gz) | +| Linux | arm64 | [minisign-linux-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-linux-arm64.tar.gz) | +| MacOS | arm64 | [minisign-darwin-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-darwin-arm64.tar.gz) | +| MacOS | amd64 | [minisign-darwin-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-darwin-amd64.tar.gz) | +| Windows | amd64 | [minisign-windows-amd64.zip](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-windows-amd64.zip) | If your system has [Go1.16+](https://golang.org/dl/), you can build from source: ``` From 46482c03861d31227cdbe008955607f67b0fe0ce Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Thu, 16 May 2024 15:05:22 +0200 Subject: [PATCH 2/9] add makefile and update `go.mod` to Go1.21 Signed-off-by: Andreas Auernhammer --- .github/workflows/go.yml | 24 +++++++++--------- Makefile | 54 ++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index cb243b3..6756b88 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.21.1] + go-version: [1.21.9, 1.22.3] steps: - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v3 @@ -23,8 +23,6 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Build and Lint - env: - GO111MODULE: on run: | go build ./... go vet ./... @@ -35,7 +33,7 @@ jobs: - name: "Set up Go" uses: actions/setup-go@v3 with: - go-version: 1.21.1 + go-version: 1.22.3 - name: Check out code uses: actions/checkout@v3 - name: Lint @@ -44,11 +42,11 @@ jobs: version: latest args: --config ./.golangci.yml --timeout=2m test: - name: Text ${{ matrix.os }} + name: Text ${{ matrix.os }} | ${{ matrix.go-version }} runs-on: ${{ matrix.os }} strategy: matrix: - go-version: [1.21.1] + go-version: [1.21.9, 1.22.3] os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }} @@ -61,16 +59,18 @@ jobs: run: | go test ./... vulncheck: - name: Vulncheck - needs: Lint + name: Vulncheck ${{ matrix.go-version }} runs-on: ubuntu-latest + strategy: + matrix: + go-version: [1.21.9, 1.22.3] steps: + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: 1.21.1 - check-latest: true - name: Get govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest shell: bash diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7d1dbbf --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +ifneq ($(shell go env GOBIN),) + GOBIN := $(shell go env GOBIN) +else + GOBIN := $(shell $(go env GOPATH)/bin) +endif +VERSION := v0.2.1 + +.PHONY: build check release test lint update-tools + +build: + @mkdir -m 0755 -p ${GOBIN} + @CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ${GOBIN}/minisign ./cmd/minisign + +check: + @gofmt -d . && echo No formatting issue found. + @govulncheck ./... + +release: +ifneq ($(shell git status -s) , ) + @(echo "Repository contains modified files." && exit 1) +else + @echo -n Building minisign ${VERSION} for linux/amd64... + @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @tar -czf minisign-linux-amd64.tar.gz ./minisign ./LICENSE ./README.md + @echo " DONE." + + @echo -n Building minisign ${VERSION} for linux/arm64... + @GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @tar -czf minisign-linux-arm64.tar.gz ./minisign ./LICENSE ./README.md + @echo " DONE." + + @echo -n Building minisign ${VERSION} for darwin/arm64... + @GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @tar -czf minisign-darwin-arm64.tar.gz ./minisign ./LICENSE ./README.md + @echo " DONE." + + @echo -n Building minisign ${VERSION} for windows/amd64... + @GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @zip -q minisign-windows-amd64.zip ./minisign ./LICENSE ./README.md + @echo " DONE." + + @rm ./minisign +endif + +test: + @CGO_ENABLED=0 go test -ldflags "-s -w" ./... + +lint: + @go vet ./... + @golangci-lint run --config ./.golangci.yml + +update-tools: + @CGO_ENABLED=0 go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @CGO_ENABLED=0 go install golang.org/x/vuln/cmd/govulncheck@latest diff --git a/go.mod b/go.mod index 5b1c832..6983af8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module aead.dev/minisign -go 1.20 +go 1.21 require ( golang.org/x/crypto v0.13.0 From 6d740d984709a109632543c8ad758a0674857058 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Thu, 16 May 2024 15:34:21 +0200 Subject: [PATCH 3/9] add support for passwordless private keys This commit adds support for private keys that are not protected by a password. The `PrivateKey` type now supports text (un)marshaling to en/decode a non-encrypted text representation. The `minisign` command now accepts a `-W` flag for generating a private key without requiring a password. --- cmd/minisign/minisign.go | 76 ++++++++----- example_test.go | 5 +- internal/testdata/minisign_unencrypted.key | 2 + minisign_test.go | 5 +- private.go | 117 +++++++++++++++++++-- private_test.go | 26 +++++ public.go | 4 +- signature.go | 4 +- 8 files changed, 190 insertions(+), 49 deletions(-) create mode 100644 internal/testdata/minisign_unencrypted.key create mode 100644 private_test.go diff --git a/cmd/minisign/minisign.go b/cmd/minisign/minisign.go index e6ae83c..415d9ff 100644 --- a/cmd/minisign/minisign.go +++ b/cmd/minisign/minisign.go @@ -23,7 +23,7 @@ import ( ) const usage = `Usage: - minisign -G [-p ] [-s ] + minisign -G [-p ] [-s ] [-W] minisign -S [-x ] [-s ] [-c ] [-t ] -m ... minisign -V [-H] [-x ] [-p | -P ] [-o] [-q | -Q ] -m minisign -R [-s ] [-p ] @@ -38,6 +38,7 @@ Options: -p Public key file (default: ./minisign.pub) -P Public key as base64 string -s Secret key file (default: $HOME/.minisign/minisign.key) + -W Do not encrypt/decrypt the secret key with a password. -x Signature file (default: .minisig) -c Add a one-line untrusted comment. -t Add a one-line trusted comment. @@ -66,6 +67,7 @@ func main() { pubKeyFileFlag string pubKeyFlag string secKeyFileFlag string + unencryptedKeyFlag bool signatureFlag string untrustedCommentFlag string trustedCommentFlag string @@ -84,6 +86,7 @@ func main() { flag.StringVar(&pubKeyFileFlag, "p", "minisign.pub", "Public key file (default: minisign.pub") flag.StringVar(&pubKeyFlag, "P", "", "Public key as base64 string") flag.StringVar(&secKeyFileFlag, "s", filepath.Join(os.Getenv("HOME"), ".minisign/minisign.key"), "Secret key file (default: $HOME/.minisign/minisign.key") + flag.BoolVar(&unencryptedKeyFlag, "W", false, "Do not encrypt/decrypt the secret key with a password") flag.StringVar(&signatureFlag, "x", "", "Signature file (default: .minisig)") flag.StringVar(&untrustedCommentFlag, "c", "", "Add a one-line untrusted comment") flag.StringVar(&trustedCommentFlag, "t", "", "Add a one-line trusted comment") @@ -102,7 +105,7 @@ func main() { switch { case keyGenFlag: - generateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag) + generateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag, unencryptedKeyFlag) case signFlag: signFiles(secKeyFileFlag, signatureFlag, untrustedCommentFlag, trustedCommentFlag, filesFlag...) case verifyFlag: @@ -115,7 +118,7 @@ func main() { } } -func generateKeyPair(secKeyFile, pubKeyFile string, force bool) { +func generateKeyPair(secKeyFile, pubKeyFile string, force, unencrypted bool) { if !force { _, err := os.Stat(secKeyFile) if err == nil { @@ -145,29 +148,38 @@ func generateKeyPair(secKeyFile, pubKeyFile string, force bool) { } } - var password string - if term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Print("Please enter a password to protect the secret key.\n\n") - password = readPassword(os.Stdin, "Enter Password: ") - passwordAgain := readPassword(os.Stdin, "Enter Password (one more time): ") - if password != passwordAgain { - log.Fatal("Error: passwords don't match") - } - } else { - password = readPassword(os.Stdin, "Enter Password: ") - } publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { log.Fatalf("Error: %v", err) } - fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") - encryptedPrivateKey, err := minisign.EncryptKey(password, privateKey) - if err != nil { - fmt.Println() - log.Fatalf("Error: %v", err) + var privateKeyBytes []byte + if unencrypted { + privateKeyBytes, err = privateKey.MarshalText() + if err != nil { + log.Fatalf("Error: %v", err) + } + } else { + var password string + if term.IsTerminal(int(os.Stdin.Fd())) { + fmt.Print("Please enter a password to protect the secret key.\n\n") + password = readPassword(os.Stdin, "Enter Password: ") + passwordAgain := readPassword(os.Stdin, "Enter Password (one more time): ") + if password != passwordAgain { + log.Fatal("Error: passwords don't match") + } + } else { + password = readPassword(os.Stdin, "Enter Password: ") + } + + fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") + privateKeyBytes, err = minisign.EncryptKey(password, privateKey) + if err != nil { + fmt.Println() + log.Fatalf("Error: %v", err) + } + fmt.Print("done\n\n") } - fmt.Print("done\n\n") fileFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC if !force { @@ -178,7 +190,7 @@ func generateKeyPair(secKeyFile, pubKeyFile string, force bool) { log.Fatalf("Error: %v", err) } defer skFile.Close() - if _, err = skFile.Write(encryptedPrivateKey); err != nil { + if _, err = skFile.Write(privateKeyBytes); err != nil { log.Fatalf("Error: %v", err) } @@ -218,19 +230,25 @@ func signFiles(secKeyFile, sigFile, untrustedComment, trustedComment string, fil } } - encryptedPrivateKey, err := os.ReadFile(secKeyFile) + privateKeyBytes, err := os.ReadFile(secKeyFile) if err != nil { log.Fatalf("Error: %v", err) } - password := readPassword(os.Stdin, "Enter password: ") - fmt.Print("Deriving a key from the password in order to decrypt the secret key... ") - privateKey, err := minisign.DecryptKey(password, encryptedPrivateKey) - if err != nil { - fmt.Println() - log.Fatalf("Error: invalid password: %v", err) + var privateKey minisign.PrivateKey + if minisign.IsEncrypted(privateKeyBytes) { + password := readPassword(os.Stdin, "Enter password: ") + + fmt.Print("Deriving a key from the password in order to decrypt the secret key... ") + privateKey, err = minisign.DecryptKey(password, privateKeyBytes) + if err != nil { + fmt.Println() + log.Fatalf("Error: invalid password: %v", err) + } + fmt.Print("done\n\n") + } else if err = privateKey.UnmarshalText(privateKeyBytes); err != nil { + log.Fatalf("Error: %v", err) } - fmt.Print("done\n\n") if sigFile != "" { if dir := filepath.Dir(sigFile); dir != "" && dir != "." && dir != "/" { diff --git a/example_test.go b/example_test.go index 9810900..06ece9c 100644 --- a/example_test.go +++ b/example_test.go @@ -8,7 +8,6 @@ import ( "crypto/rand" "fmt" "io" - "io/ioutil" "strconv" "strings" @@ -160,7 +159,7 @@ func ExampleReader() { // Sign a data stream after processing it. (Here, we just discard it) reader := minisign.NewReader(strings.NewReader(Message)) - if _, err := io.Copy(ioutil.Discard, reader); err != nil { + if _, err := io.Copy(io.Discard, reader); err != nil { panic(err) // TODO: error handling } signature := reader.Sign(privateKey) @@ -168,7 +167,7 @@ func ExampleReader() { // Read a data stream and then verify its authenticity with // the public key. reader = minisign.NewReader(strings.NewReader(Message)) - message, err := ioutil.ReadAll(reader) + message, err := io.ReadAll(reader) if err != nil { panic(err) // TODO: error handling } diff --git a/internal/testdata/minisign_unencrypted.key b/internal/testdata/minisign_unencrypted.key new file mode 100644 index 0000000..d1fcc14 --- /dev/null +++ b/internal/testdata/minisign_unencrypted.key @@ -0,0 +1,2 @@ +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/minisign_test.go b/minisign_test.go index e180132..e266c42 100644 --- a/minisign_test.go +++ b/minisign_test.go @@ -6,7 +6,6 @@ package minisign import ( "io" - "io/ioutil" "os" "testing" ) @@ -18,7 +17,7 @@ func TestRoundtrip(t *testing.T) { t.Fatalf("Failed to load private key: %v", err) } - message, err := ioutil.ReadFile("./internal/testdata/message.txt") + message, err := os.ReadFile("./internal/testdata/message.txt") if err != nil { t.Fatalf("Failed to load message: %v", err) } @@ -48,7 +47,7 @@ func TestReaderRoundtrip(t *testing.T) { defer file.Close() reader := NewReader(file) - if _, err = io.Copy(ioutil.Discard, reader); err != nil { + if _, err = io.Copy(io.Discard, reader); err != nil { t.Fatalf("Failed to read message: %v", err) } signature := reader.Sign(privateKey) diff --git a/private.go b/private.go index ec1fe63..d439fa1 100644 --- a/private.go +++ b/private.go @@ -5,6 +5,7 @@ package minisign import ( + "bytes" "crypto" "crypto/ed25519" "crypto/rand" @@ -12,8 +13,9 @@ import ( "encoding/base64" "encoding/binary" "errors" + "fmt" "io" - "io/ioutil" + "os" "strconv" "strings" "time" @@ -25,7 +27,7 @@ import ( // PrivateKeyFromFile reads and decrypts the private key // file with the given password. func PrivateKeyFromFile(password, path string) (PrivateKey, error) { - bytes, err := ioutil.ReadFile(path) + bytes, err := os.ReadFile(path) if err != nil { return PrivateKey{}, err } @@ -34,8 +36,7 @@ func PrivateKeyFromFile(password, path string) (PrivateKey, error) { // PrivateKey is a minisign private key. // -// A private key can sign messages to prove the -// their origin and authenticity. +// A private key can sign messages to prove their origin and authenticity. // // PrivateKey implements the crypto.Signer interface. type PrivateKey struct { @@ -101,9 +102,89 @@ func (p PrivateKey) Equal(x crypto.PrivateKey) bool { return p.id == xx.id && subtle.ConstantTimeCompare(p.bytes[:], xx.bytes[:]) == 1 } +// MarshalText returns a textual representation of the private key. +// +// For password-protected private keys refer to [EncryptKey]. +func (p PrivateKey) MarshalText() ([]byte, error) { + var b [privateKeySize]byte + + binary.LittleEndian.PutUint16(b[:], EdDSA) + binary.LittleEndian.PutUint16(b[2:], algorithmNone) + binary.LittleEndian.PutUint16(b[4:], algorithmBlake2b) + + binary.LittleEndian.PutUint64(b[54:], p.id) + copy(b[62:], p.bytes[:]) + + const comment = "untrusted comment: minisign encrypted secret key\n" + encodedBytes := make([]byte, len(comment)+base64.StdEncoding.EncodedLen(len(b))) + copy(encodedBytes, []byte(comment)) + base64.StdEncoding.Encode(encodedBytes[len(comment):], b[:]) + return encodedBytes, nil +} + +// UnmarshalText decodes a textual representation of the private key into p. +// +// It returns an error if the private key is encrypted. For decrypting +// password-protected private keys refer to [DecryptKey]. +func (p *PrivateKey) UnmarshalText(text []byte) error { + text = trimUntrustedComment(text) + b := make([]byte, base64.StdEncoding.DecodedLen(len(text))) + n, err := base64.StdEncoding.Decode(b, text) + if err != nil { + return fmt.Errorf("minisign: invalid private key: %v", err) + } + b = b[:n] + + if len(b) != privateKeySize { + return errors.New("minisign: invalid private key") + } + + var ( + empty [32]byte + + kType = binary.LittleEndian.Uint16(b) + kdf = binary.LittleEndian.Uint16(b[2:]) + hType = binary.LittleEndian.Uint16(b[4:]) + salt = b[6:38] + scryptOps = binary.LittleEndian.Uint64(b[38:]) + scryptMem = binary.LittleEndian.Uint64(b[46:]) + key = b[54:126] + checksum = b[126:privateKeySize] + ) + if kType != EdDSA { + return fmt.Errorf("minisign: invalid private key: invalid key type '%d'", kType) + } + if kdf == algorithmScrypt { + return errors.New("minisign: private key is encrypted") + } + if kdf != algorithmNone { + return fmt.Errorf("minisign: invalid private key: invalid KDF '%d'", kdf) + } + if hType != algorithmBlake2b { + return fmt.Errorf("minisign: invalid private key: invalid hash type '%d'", hType) + } + if !bytes.Equal(salt[:], empty[:]) { + return errors.New("minisign: invalid private key: salt is not empty") + } + if scryptOps != 0 { + return errors.New("minisign: invalid private key: scrypt cost parameter is not zero") + } + if scryptMem != 0 { + return errors.New("minisign: invalid private key: scrypt mem parameter is not zero") + } + if !bytes.Equal(checksum, empty[:]) { + return errors.New("minisign: invalid private key: salt is not empty") + } + + p.id = binary.LittleEndian.Uint64(key[:8]) + copy(p.bytes[:], key[8:]) + return nil +} + const ( - scryptAlgorithm = 0x6353 // hex value for "Sc" - blake2bAlgorithm = 0x3242 // hex value for "B2" + algorithmNone = 0x0000 // hex value for KDF when key is not encrypted + algorithmScrypt = 0x6353 // hex value for "Sc" + algorithmBlake2b = 0x3242 // hex value for "B2" scryptOpsLimit = 0x2000000 // max. Scrypt ops limit based on libsodium scryptMemLimit = 0x40000000 // max. Scrypt mem limit based on libsodium @@ -125,8 +206,8 @@ func EncryptKey(password string, privateKey PrivateKey) ([]byte, error) { var bytes [privateKeySize]byte binary.LittleEndian.PutUint16(bytes[0:], EdDSA) - binary.LittleEndian.PutUint16(bytes[2:], scryptAlgorithm) - binary.LittleEndian.PutUint16(bytes[4:], blake2bAlgorithm) + binary.LittleEndian.PutUint16(bytes[2:], algorithmScrypt) + binary.LittleEndian.PutUint16(bytes[4:], algorithmBlake2b) const ( // TODO(aead): Callers may want to customize the cost parameters defaultOps = 33554432 // libsodium OPS_LIMIT_SENSITIVE @@ -144,6 +225,22 @@ func EncryptKey(password string, privateKey PrivateKey) ([]byte, error) { return encodedBytes, nil } +// IsEncrypted reports whether the private key is encrypted. +func IsEncrypted(privateKey []byte) bool { + privateKey = trimUntrustedComment(privateKey) + bytes := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey))) + n, err := base64.StdEncoding.Decode(bytes, privateKey) + if err != nil { + return false + } + bytes = bytes[:n] + + if len(bytes) != privateKeySize { + return false + } + return binary.LittleEndian.Uint16(bytes[2:4]) == algorithmScrypt +} + var errDecrypt = errors.New("minisign: decryption failed") // DecryptKey tries to decrypt the encrypted private key with @@ -163,10 +260,10 @@ func DecryptKey(password string, privateKey []byte) (PrivateKey, error) { if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA { return PrivateKey{}, errDecrypt } - if a := binary.LittleEndian.Uint16(bytes[2:4]); a != scryptAlgorithm { + if a := binary.LittleEndian.Uint16(bytes[2:4]); a != algorithmScrypt { return PrivateKey{}, errDecrypt } - if a := binary.LittleEndian.Uint16(bytes[4:6]); a != blake2bAlgorithm { + if a := binary.LittleEndian.Uint16(bytes[4:6]); a != algorithmBlake2b { return PrivateKey{}, errDecrypt } diff --git a/private_test.go b/private_test.go new file mode 100644 index 0000000..6db20c1 --- /dev/null +++ b/private_test.go @@ -0,0 +1,26 @@ +// Copyright (c) 2024 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 + +import ( + "bytes" + "os" + "testing" +) + +func TestPrivateKey_Unmarshal(t *testing.T) { + raw, err := os.ReadFile("./internal/testdata/minisign_unencrypted.key") + if err != nil { + t.Fatalf("Failed to read private key: %v", err) + } + + keys := bytes.Split(raw, []byte("\n\n")) // Private keys are separated by a newline + for _, k := range keys { + var key PrivateKey + if err := key.UnmarshalText(k); err != nil { + t.Fatalf("Failed to unmarshal private key: %v\nPrivate key:\n%s", err, string(k)) + } + } +} diff --git a/public.go b/public.go index 61dd45e..ac83b1c 100644 --- a/public.go +++ b/public.go @@ -11,7 +11,7 @@ import ( "encoding/binary" "errors" "fmt" - "io/ioutil" + "os" "strconv" "strings" ) @@ -19,7 +19,7 @@ import ( // PublicKeyFromFile reads a new PublicKey from the // given file. func PublicKeyFromFile(path string) (PublicKey, error) { - bytes, err := ioutil.ReadFile(path) + bytes, err := os.ReadFile(path) if err != nil { return PublicKey{}, err } diff --git a/signature.go b/signature.go index 78feecd..42e2f1f 100644 --- a/signature.go +++ b/signature.go @@ -10,7 +10,7 @@ import ( "encoding/binary" "errors" "fmt" - "io/ioutil" + "os" "strconv" "strings" ) @@ -18,7 +18,7 @@ import ( // SignatureFromFile reads a new Signature from the // given file. func SignatureFromFile(file string) (Signature, error) { - bytes, err := ioutil.ReadFile(file) + bytes, err := os.ReadFile(file) if err != nil { return Signature{}, err } From 0814912d98c1c7a378948d0d3a801a0831a83f55 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Fri, 17 May 2024 09:31:30 +0200 Subject: [PATCH 4/9] improve docs and implementation of `Signature`, `PrivateKey` and `PublicKey` This commit improves the documentation for the core minisign types and improves the implementation for various functions. It also adds an additional test vector for (un)marshaling private keys. Signed-off-by: Andreas Auernhammer --- .golangci.yml | 26 ++++---- internal/testdata/minisign_unencrypted.key | 2 - internal/testdata/unencrypted.key | 5 ++ minisign.pub | 2 + private.go | 58 +++++++++-------- private_test.go | 71 ++++++++++++++++++++- public.go | 28 +++++---- signature.go | 73 ++++++++++++---------- 8 files changed, 177 insertions(+), 88 deletions(-) delete mode 100644 internal/testdata/minisign_unencrypted.key create mode 100644 internal/testdata/unencrypted.key create mode 100644 minisign.pub diff --git a/.golangci.yml b/.golangci.yml index afd2b48..eb84221 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,4 @@ linters-settings: - golint: - min-confidence: 0 - misspell: locale: US @@ -11,24 +8,27 @@ linters-settings: linters: disable-all: true enable: - - typecheck + - durationcheck + - gocritic + - gofmt - goimports - - misspell - - staticcheck + - gomodguard - govet - - revive - ineffassign - - gosimple - - unused - - prealloc + - misspell + - revive + - staticcheck + - tenv + - typecheck - unconvert - - gofumpt + - unused issues: exclude-use-default: false exclude: - - "var-naming: don't use ALL_CAPS in Go names; use CamelCase" - "package-comments: should have a package comment" + - "exitAfterDefer:" + - "captLocal:" service: - golangci-lint-version: 1.48.0 # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.57.2 # use the fixed version to not introduce new linters unexpectedly diff --git a/internal/testdata/minisign_unencrypted.key b/internal/testdata/minisign_unencrypted.key deleted file mode 100644 index d1fcc14..0000000 --- a/internal/testdata/minisign_unencrypted.key +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: minisign encrypted secret key -RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/internal/testdata/unencrypted.key b/internal/testdata/unencrypted.key new file mode 100644 index 0000000..2b92a65 --- /dev/null +++ b/internal/testdata/unencrypted.key @@ -0,0 +1,5 @@ +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb/yydu4x5dcvbgaLZRtY5v8wFvgzMkvKyALUXUWcT+bvaqFvuvkUyUfMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/minisign.pub b/minisign.pub new file mode 100644 index 0000000..e719291 --- /dev/null +++ b/minisign.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key D7E531EE76B2FC6F +RWRv/LJ27jHl10fMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0 diff --git a/private.go b/private.go index d439fa1..676d722 100644 --- a/private.go +++ b/private.go @@ -106,6 +106,8 @@ func (p PrivateKey) Equal(x crypto.PrivateKey) bool { // // For password-protected private keys refer to [EncryptKey]. func (p PrivateKey) MarshalText() ([]byte, error) { + // A non-encrypted private key has the same format as an encrypted one. + // However, the salt, and auth. tag are set to all zero. var b [privateKeySize]byte binary.LittleEndian.PutUint16(b[:], EdDSA) @@ -115,6 +117,8 @@ func (p PrivateKey) MarshalText() ([]byte, error) { binary.LittleEndian.PutUint64(b[54:], p.id) copy(b[62:], p.bytes[:]) + // It seems odd that the comment says: "encrypted secret key". + // However, the original C implementation behaves like this. const comment = "untrusted comment: minisign encrypted secret key\n" encodedBytes := make([]byte, len(comment)+base64.StdEncoding.EncodedLen(len(b))) copy(encodedBytes, []byte(comment)) @@ -140,7 +144,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { } var ( - empty [32]byte + empty [32]byte // For checking that the salt/tag are empty kType = binary.LittleEndian.Uint16(b) kdf = binary.LittleEndian.Uint16(b[2:]) @@ -149,7 +153,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { scryptOps = binary.LittleEndian.Uint64(b[38:]) scryptMem = binary.LittleEndian.Uint64(b[46:]) key = b[54:126] - checksum = b[126:privateKeySize] + tag = b[126:privateKeySize] ) if kType != EdDSA { return fmt.Errorf("minisign: invalid private key: invalid key type '%d'", kType) @@ -163,7 +167,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { if hType != algorithmBlake2b { return fmt.Errorf("minisign: invalid private key: invalid hash type '%d'", hType) } - if !bytes.Equal(salt[:], empty[:]) { + if !bytes.Equal(salt, empty[:]) { return errors.New("minisign: invalid private key: salt is not empty") } if scryptOps != 0 { @@ -172,11 +176,11 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { if scryptMem != 0 { return errors.New("minisign: invalid private key: scrypt mem parameter is not zero") } - if !bytes.Equal(checksum, empty[:]) { + if !bytes.Equal(tag, empty[:]) { return errors.New("minisign: invalid private key: salt is not empty") } - p.id = binary.LittleEndian.Uint64(key[:8]) + p.id = binary.LittleEndian.Uint64(key) copy(p.bytes[:], key[8:]) return nil } @@ -235,10 +239,7 @@ func IsEncrypted(privateKey []byte) bool { } bytes = bytes[:n] - if len(bytes) != privateKeySize { - return false - } - return binary.LittleEndian.Uint16(bytes[2:4]) == algorithmScrypt + return len(bytes) >= 4 && binary.LittleEndian.Uint16(bytes[2:]) == algorithmScrypt } var errDecrypt = errors.New("minisign: decryption failed") @@ -247,47 +248,50 @@ var errDecrypt = errors.New("minisign: decryption failed") // the given password. func DecryptKey(password string, privateKey []byte) (PrivateKey, error) { privateKey = trimUntrustedComment(privateKey) - bytes := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey))) - n, err := base64.StdEncoding.Decode(bytes, privateKey) + b := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey))) + n, err := base64.StdEncoding.Decode(b, privateKey) if err != nil { return PrivateKey{}, err } - bytes = bytes[:n] + b = b[:n] - if len(bytes) != privateKeySize { + if len(b) != privateKeySize { return PrivateKey{}, errDecrypt } - if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA { + var ( + kType = binary.LittleEndian.Uint16(b) + kdf = binary.LittleEndian.Uint16(b[2:]) + hType = binary.LittleEndian.Uint16(b[4:]) + salt = b[6:38] + scryptOps = binary.LittleEndian.Uint64(b[38:]) + scryptMem = binary.LittleEndian.Uint64(b[46:]) + ciphertext = b[54:] + ) + if kType != EdDSA { return PrivateKey{}, errDecrypt } - if a := binary.LittleEndian.Uint16(bytes[2:4]); a != algorithmScrypt { + if kdf != algorithmScrypt { return PrivateKey{}, errDecrypt } - if a := binary.LittleEndian.Uint16(bytes[4:6]); a != algorithmBlake2b { + if hType != algorithmBlake2b { return PrivateKey{}, errDecrypt } - - var ( - scryptOps = binary.LittleEndian.Uint64(bytes[38:46]) - scryptMem = binary.LittleEndian.Uint64(bytes[46:54]) - ) if scryptOps > scryptOpsLimit { return PrivateKey{}, errDecrypt } if scryptMem > scryptMemLimit { return PrivateKey{}, errDecrypt } - var salt [32]byte - copy(salt[:], bytes[6:38]) - privateKeyBytes, err := decryptKey(password, salt[:], scryptOps, scryptMem, bytes[54:]) + + plaintext, err := decryptKey(password, salt, scryptOps, scryptMem, ciphertext) if err != nil { return PrivateKey{}, err } key := PrivateKey{ - id: binary.LittleEndian.Uint64(privateKeyBytes[:8]), + id: binary.LittleEndian.Uint64(plaintext), } - copy(key.bytes[:], privateKeyBytes[8:]) + copy(key.bytes[:], plaintext[8:]) return key, nil } @@ -368,7 +372,7 @@ func decryptKey(password string, salt []byte, ops, mem uint64, ciphertext []byte binary.LittleEndian.PutUint16(message[:2], EdDSA) copy(message[2:], privateKeyBytes) - if sum := blake2b.Sum256(message[:]); subtle.ConstantTimeCompare(sum[:], checksum[:]) != 1 { + if sum := blake2b.Sum256(message[:]); subtle.ConstantTimeCompare(sum[:], checksum) != 1 { return nil, errDecrypt } return privateKeyBytes, nil diff --git a/private_test.go b/private_test.go index 6db20c1..3a2f7de 100644 --- a/private_test.go +++ b/private_test.go @@ -6,21 +6,88 @@ package minisign import ( "bytes" + "encoding/base64" "os" + "strconv" "testing" ) +var marshalPrivateKeyTests = []struct { + ID uint64 + Bytes []byte +}{ + { + ID: htoi("3728470A8118E56E"), + Bytes: b64("JpjEI/XIKqIVl99tT611AxXwlVjlw2afJC8Nv6o7uuipyNvC3DmgO2csDT+bw1bZR3ss4rd5cXqoq0uftlCJqw=="), + }, + { + ID: htoi("D7E531EE76B2FC6F"), + Bytes: b64("L24Gi2UbWOb/MBb4MzJLysgC1F1FnE/m72qhb7r5FMlHzHe6M6mCLPMzmj6ln+hI51kqpDqTkIg9VCaToAhZtA=="), + }, +} + +func TestPrivateKey_Marshal(t *testing.T) { + raw, err := os.ReadFile("./internal/testdata/unencrypted.key") + if err != nil { + t.Fatalf("Failed to read private key: %v", err) + } + raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) + raw = bytes.TrimSuffix(raw, []byte{'\n'}) + + keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline + if len(keys) != len(marshalPrivateKeyTests) { + t.Fatalf("Test vectors don't match: got %d - want %d", len(marshalPrivateKeyTests), len(keys)) + } + for i, test := range marshalPrivateKeyTests { + key := PrivateKey{ + id: test.ID, + } + copy(key.bytes[:], test.Bytes) + + text, err := key.MarshalText() + if err != nil { + t.Fatalf("Test %d: failed to marshal private key: %v", i, err) + } + if !bytes.Equal(text, keys[i]) { + t.Log(len(text), len(keys[i])) + t.Log(string(keys[i][len(keys[i])-1])) + t.Fatalf("Test %d: failed to marshal private key:\nGot: %v\nWant: %v\n", i, text, keys[i]) + } + } +} + func TestPrivateKey_Unmarshal(t *testing.T) { - raw, err := os.ReadFile("./internal/testdata/minisign_unencrypted.key") + raw, err := os.ReadFile("./internal/testdata/unencrypted.key") if err != nil { t.Fatalf("Failed to read private key: %v", err) } + raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) + raw = bytes.TrimSuffix(raw, []byte{'\n'}) - keys := bytes.Split(raw, []byte("\n\n")) // Private keys are separated by a newline + keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline for _, k := range keys { var key PrivateKey if err := key.UnmarshalText(k); err != nil { t.Fatalf("Failed to unmarshal private key: %v\nPrivate key:\n%s", err, string(k)) } + + // Print test vector for marshaling: + // t.Logf("\n{\n\tID: htoi(\"%X\"),\n\tBytes: b64(\"%s\"),\n}", key.id, base64.StdEncoding.EncodeToString(key.bytes[:])) + } +} + +func htoi(s string) uint64 { + i, err := strconv.ParseUint(s, 16, 64) + if err != nil { + panic(err) + } + return i +} + +func b64(s string) []byte { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + panic(err) } + return b } diff --git a/public.go b/public.go index ac83b1c..0d86e0f 100644 --- a/public.go +++ b/public.go @@ -16,10 +16,11 @@ import ( "strings" ) -// PublicKeyFromFile reads a new PublicKey from the -// given file. -func PublicKeyFromFile(path string) (PublicKey, error) { - bytes, err := os.ReadFile(path) +const publicKeySize = 2 + 8 + ed25519.PublicKeySize + +// PublicKeyFromFile reads a PublicKey from the given file. +func PublicKeyFromFile(filename string) (PublicKey, error) { + bytes, err := os.ReadFile(filename) if err != nil { return PublicKey{}, err } @@ -57,7 +58,7 @@ func (p PublicKey) Equal(x crypto.PublicKey) bool { // String returns a base64 string representation of the PublicKey p. func (p PublicKey) String() string { - var bytes [2 + 8 + ed25519.PublicKeySize]byte + var bytes [publicKeySize]byte binary.LittleEndian.PutUint16(bytes[:2], EdDSA) binary.LittleEndian.PutUint64(bytes[2:10], p.ID()) copy(bytes[10:], p.bytes[:]) @@ -69,12 +70,17 @@ func (p PublicKey) String() string { // // It never returns an error. func (p PublicKey) MarshalText() ([]byte, error) { - comment := "untrusted comment: minisign public key: " + strings.ToUpper(strconv.FormatUint(p.ID(), 16)) + "\n" - return []byte(comment + p.String()), nil + s := make([]byte, 0, 113) // Size of a public key in text format + s = append(s, "untrusted comment: minisign public key: "...) + s = append(s, strings.ToUpper(strconv.FormatUint(p.ID(), 16))...) + s = append(s, '\n') + s = append(s, p.String()...) + return s, nil } -// UnmarshalText parses text as textual-encoded public key. -// It returns an error if text is not a well-formed public key. +// UnmarshalText decodes a textual representation of a public key into p. +// +// It returns an error in case of a malformed key. func (p *PublicKey) UnmarshalText(text []byte) error { text = trimUntrustedComment(text) bytes := make([]byte, base64.StdEncoding.DecodedLen(len(text))) @@ -82,9 +88,9 @@ func (p *PublicKey) UnmarshalText(text []byte) error { if err != nil { return fmt.Errorf("minisign: invalid public key: %v", err) } - bytes = bytes[:n] // Adjust b/c text may contain '\r' or '\n' which would have been ignored during decoding. + bytes = bytes[:n] // Adjust since text may contain '\r' or '\n' which would have been ignored during decoding. - if n = len(bytes); n != 2+8+ed25519.PublicKeySize { + if n = len(bytes); n != publicKeySize { return errors.New("minisign: invalid public key length " + strconv.Itoa(n)) } if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA { diff --git a/signature.go b/signature.go index 42e2f1f..112f116 100644 --- a/signature.go +++ b/signature.go @@ -15,10 +15,9 @@ import ( "strings" ) -// SignatureFromFile reads a new Signature from the -// given file. -func SignatureFromFile(file string) (Signature, error) { - bytes, err := os.ReadFile(file) +// SignatureFromFile reads a Signature from the given file. +func SignatureFromFile(filename string) (Signature, error) { + bytes, err := os.ReadFile(filename) if err != nil { return Signature{}, err } @@ -48,8 +47,7 @@ func SignatureFromFile(file string) (Signature, error) { type Signature struct { _ [0]func() // enforce named assignment and prevent direct comparison - // Algorithm is the signature algorithm. It is either - // EdDSA or HashEdDSA. + // Algorithm is the signature algorithm. It is either EdDSA or HashEdDSA. Algorithm uint16 // KeyID may be the 64 bit ID of the private key that was used @@ -89,27 +87,7 @@ type Signature struct { // In contrast to MarshalText, String does not fail if s is // not a valid minisign signature. func (s Signature) String() string { - var buffer strings.Builder - buffer.WriteString("untrusted comment: ") - buffer.WriteString(s.UntrustedComment) - buffer.WriteByte('\n') - - var signature [2 + 8 + ed25519.SignatureSize]byte - binary.LittleEndian.PutUint16(signature[:2], s.Algorithm) - binary.LittleEndian.PutUint64(signature[2:10], s.KeyID) - copy(signature[10:], s.Signature[:]) - - buffer.WriteString(base64.StdEncoding.EncodeToString(signature[:])) - buffer.WriteByte('\n') - - buffer.WriteString("trusted comment: ") - buffer.WriteString(s.TrustedComment) - buffer.WriteByte('\n') - - buffer.WriteString(base64.StdEncoding.EncodeToString(s.CommentSignature[:])) - buffer.WriteByte('\n') - - return buffer.String() + return string(encodeSignature(&s)) } // Equal reports whether s and x have equivalent values. @@ -125,18 +103,18 @@ func (s Signature) Equal(x Signature) bool { // MarshalText returns a textual representation of the Signature s. // -// It returns an error if s cannot be a valid signature - e.g. -// because the signature algorithm is neither EdDSA nor HashEdDSA. +// It returns an error if s cannot be a valid signature, for example. +// when s.Algorithm is neither EdDSA nor HashEdDSA. func (s Signature) MarshalText() ([]byte, error) { if s.Algorithm != EdDSA && s.Algorithm != HashEdDSA { return nil, errors.New("minisign: invalid signature algorithm " + strconv.Itoa(int(s.Algorithm))) } - return []byte(s.String()), nil + return encodeSignature(&s), nil } -// UnmarshalText parses text as textual-encoded signature. -// It returns an error if text is not a well-formed minisign -// signature. +// UnmarshalText decodes a textual representation of a signature into s. +// +// It returns an error in case of a malformed signature. func (s *Signature) UnmarshalText(text []byte) error { segments := strings.SplitN(string(text), "\n", 4) if len(segments) != 4 { @@ -187,3 +165,32 @@ func (s *Signature) UnmarshalText(text []byte) error { copy(s.CommentSignature[:], commentSignature) return nil } + +// encodeSignature encodes s into its textual representation. +func encodeSignature(s *Signature) []byte { + var signature [2 + 8 + ed25519.SignatureSize]byte + binary.LittleEndian.PutUint16(signature[:], s.Algorithm) + binary.LittleEndian.PutUint64(signature[2:], s.KeyID) + copy(signature[10:], s.Signature[:]) + + b := make([]byte, 0, 228+len(s.TrustedComment)+len(s.UntrustedComment)) // Size of a signature in text format + b = append(b, "untrusted comment: "...) + b = append(b, s.UntrustedComment...) + b = append(b, '\n') + + // TODO(aead): use base64.StdEncoding.EncodeAppend once Go1.21 is dropped + n := len(b) + b = b[:n+base64.StdEncoding.EncodedLen(len(signature))] + base64.StdEncoding.Encode(b[n:], signature[:]) + b = append(b, '\n') + + b = append(b, "trusted comment: "...) + b = append(b, s.TrustedComment...) + b = append(b, '\n') + + // TODO(aead): use base64.StdEncoding.EncodeAppend once Go1.21 is dropped + n = len(b) + b = b[:n+base64.StdEncoding.EncodedLen(len(s.CommentSignature))] + base64.StdEncoding.Encode(b[n:], s.CommentSignature[:]) + return append(b, '\n') +} From 7803913be0bb856e44020912ee6c2b942c36ecdd Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Fri, 17 May 2024 10:42:02 +0200 Subject: [PATCH 5/9] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index afd1ca3..4acf8f5 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,7 @@ On windows, linux and macOS, you can use the pre-built binaries: If your system has [Go1.16+](https://golang.org/dl/), you can build from source: ``` -git clone https://aead.dev/minisign && cd minisign -go build -o . aead.dev/minisign/cmd/minisign +git clone https://aead.dev/minisign && cd minisign && make build ``` ## Library From 212c10bb158d7a980fe6a7734c94da0aacd585fd Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Sat, 18 May 2024 14:14:13 +0200 Subject: [PATCH 6/9] add `-C` flag for changing private key password This commit adds the `-C` flag for changing private key passwords. It also improves the entire minisign command implementation, removes some redunant code and fixes minor tui issues. --- cmd/minisign/minisign.go | 624 ++++++++++-------- internal/testdata/unencrypted-0.key | 2 + .../{unencrypted.key => unencrypted-1.key} | 6 +- private.go | 27 +- private_test.go | 49 +- 5 files changed, 399 insertions(+), 309 deletions(-) create mode 100644 internal/testdata/unencrypted-0.key rename internal/testdata/{unencrypted.key => unencrypted-1.key} (50%) diff --git a/cmd/minisign/minisign.go b/cmd/minisign/minisign.go index 415d9ff..27ee50c 100644 --- a/cmd/minisign/minisign.go +++ b/cmd/minisign/minisign.go @@ -11,7 +11,6 @@ import ( "flag" "fmt" "io" - "log" "os" "path/filepath" "runtime" @@ -22,14 +21,19 @@ import ( "golang.org/x/term" ) +const version = "v0.2.1" + const usage = `Usage: minisign -G [-p ] [-s ] [-W] + minisign -R [-s ] [-p ] + minisign -C [-s ] [-W] minisign -S [-x ] [-s ] [-c ] [-t ] -m ... minisign -V [-H] [-x ] [-p | -P ] [-o] [-q | -Q ] -m - minisign -R [-s ] [-p ] Options: -G Generate a new public/secret key pair. + -R Re-create a public key file from a secret key. + -C Change or remove the password of the secret key. -S Sign files with a secret key. -V Verify files with a public key. -m The file to sign or verify. @@ -44,387 +48,427 @@ Options: -t Add a one-line trusted comment. -q Quiet mode. Suppress output. -Q Pretty quiet mode. Combined with -V, only print the trusted comment. - -R Re-create a public key file from a secret key. -f Combined with -G or -R, overwrite any existing public/secret key pair. -v Print version information. ` -var version = "v0.0.0-dev" +var ( + flagKeyGen bool // Generate a new key pair. + flagRestore bool // Restore a public key from a private key + flagChangePassword bool // Update/Remove private key password + flagSign bool // Sign files + flagVerify bool // Verify signatures + + flagPrivateKeyFile string // Path to private key file + flagPublicKeyFile string // Path to public key flile + flagPublicKey string // Public key. Takes precedence over public key file + flagFiles = filenames{} // List of files to sign/verify + flagSignatureFile string // Custom signature file. Defaults to .minisig + + flagTrustedComment string // Custom comment that is signed and verified + flagUntrustedComment string // Custom comment that is NOT signed NOR verified + + flagOutput bool // Output files when verified successfully + flagPreHash bool // Verify legacy signatures when files where pre-hashed + flagWithoutPassword bool // Whether a private key should be password-protected + flagPrettyQuiet bool // Suppress output except for trusted comment after verification + flagQuiet bool // Suppress all output + flagForce bool // Overwrite existing private/public keys + flagVersion bool // Print version information +) func main() { - log.SetFlags(0) - log.SetOutput(os.Stderr) - flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } - var ( - keyGenFlag bool - signFlag bool - verifyFlag bool - filesFlag = multiFlag{} - outputFlag bool - hashFlag bool - pubKeyFileFlag string - pubKeyFlag string - secKeyFileFlag string - unencryptedKeyFlag bool - signatureFlag string - untrustedCommentFlag string - trustedCommentFlag string - quietFlag bool - prettyQuietFlag bool - recreateFlag bool - forceFlag bool - versionFlag bool - ) - flag.BoolVar(&keyGenFlag, "G", false, "Generate a new public/secret key pair") - flag.BoolVar(&signFlag, "S", false, "Sign files with a secret key") - flag.BoolVar(&verifyFlag, "V", false, "Verify files with a public key") - flag.Var(&filesFlag, "m", "One or multiple files to sign or verfiy") - flag.BoolVar(&outputFlag, "o", false, "Combined with -V, output the file after verification") - flag.BoolVar(&hashFlag, "H", false, "Combined with -S, pre-hash in order to sign large files") - flag.StringVar(&pubKeyFileFlag, "p", "minisign.pub", "Public key file (default: minisign.pub") - flag.StringVar(&pubKeyFlag, "P", "", "Public key as base64 string") - flag.StringVar(&secKeyFileFlag, "s", filepath.Join(os.Getenv("HOME"), ".minisign/minisign.key"), "Secret key file (default: $HOME/.minisign/minisign.key") - flag.BoolVar(&unencryptedKeyFlag, "W", false, "Do not encrypt/decrypt the secret key with a password") - flag.StringVar(&signatureFlag, "x", "", "Signature file (default: .minisig)") - flag.StringVar(&untrustedCommentFlag, "c", "", "Add a one-line untrusted comment") - flag.StringVar(&trustedCommentFlag, "t", "", "Add a one-line trusted comment") - flag.BoolVar(&quietFlag, "q", false, "Quiet mode. Suppress output") - flag.BoolVar(&prettyQuietFlag, "Q", false, "Pretty quiet mode. Combined with -V, only print the trusted comment") - flag.BoolVar(&recreateFlag, "R", false, "Re-create a public key file from a secret key") - flag.BoolVar(&forceFlag, "f", false, "Combined with -G, overwrite any existing public/secret key pair") - flag.BoolVar(&versionFlag, "v", false, "Print version information") + flag.BoolVar(&flagKeyGen, "G", false, "") + flag.BoolVar(&flagRestore, "R", false, "") + flag.BoolVar(&flagChangePassword, "C", false, "") + flag.BoolVar(&flagSign, "S", false, "") + flag.BoolVar(&flagVerify, "V", false, "") + + flag.StringVar(&flagPrivateKeyFile, "s", filepath.Join(homedir(), ".minisign/minisign.key"), "") + flag.StringVar(&flagPublicKeyFile, "p", "minisign.pub", "") + flag.StringVar(&flagPublicKey, "P", "", "") + flag.Var(&flagFiles, "m", "") + flag.StringVar(&flagSignatureFile, "x", "", "") + + flag.StringVar(&flagTrustedComment, "t", "", "") + flag.StringVar(&flagUntrustedComment, "c", "", "") + + flag.BoolVar(&flagOutput, "o", false, "") + flag.BoolVar(&flagPreHash, "H", false, "") + flag.BoolVar(&flagWithoutPassword, "W", false, "") + flag.BoolVar(&flagPrettyQuiet, "Q", false, "") + flag.BoolVar(&flagQuiet, "q", false, "") + flag.BoolVar(&flagForce, "f", false, "") + flag.BoolVar(&flagVersion, "v", false, "") + os.Args = append(os.Args[:1:1], expandFlags(os.Args[1:])...) // Expand flags to parse combined flags '-Vm' or '-Gf' properly flag.Parse() - if versionFlag { + if flagVersion { fmt.Printf("minisign %s on %s-%s\n", version, runtime.GOOS, runtime.GOARCH) return } switch { - case keyGenFlag: - generateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag, unencryptedKeyFlag) - case signFlag: - signFiles(secKeyFileFlag, signatureFlag, untrustedCommentFlag, trustedCommentFlag, filesFlag...) - case verifyFlag: - verifyFile(signatureFlag, pubKeyFileFlag, pubKeyFlag, outputFlag, quietFlag, prettyQuietFlag, hashFlag, filesFlag...) - case recreateFlag: - recreateKeyPair(secKeyFileFlag, pubKeyFileFlag, forceFlag) + case flagKeyGen: + generateKeyPair() + case flagRestore: + restorePublicKey() + case flagChangePassword: + changePassword() + case flagSign: + signFiles() + case flagVerify: + verifyFile() default: flag.Usage() os.Exit(1) } } -func generateKeyPair(secKeyFile, pubKeyFile string, force, unencrypted bool) { - if !force { - _, err := os.Stat(secKeyFile) - if err == nil { - log.Fatalf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", secKeyFile) - } - if err != nil && !errors.Is(err, os.ErrNotExist) { - log.Fatalf("Error: %v", err) - } +func generateKeyPair() { + // Create private and public key parent directories + mkdirs(filepath.Dir(flagPrivateKeyFile)) + mkdirs(filepath.Dir(flagPublicKeyFile)) - _, err = os.Stat(pubKeyFile) - if err == nil { - log.Fatalf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", pubKeyFile) - } - if err != nil && !errors.Is(err, os.ErrNotExist) { - log.Fatalf("Error: %v", err) - } - } - - if dir := filepath.Dir(secKeyFile); dir != "" && dir != "." && dir != "/" { - if err := os.MkdirAll(dir, 0o755); err != nil { - log.Fatalf("Error: %v", err) + // Check whether private / public key already exists + if !flagForce { + if _, err := os.Stat(flagPrivateKeyFile); !errors.Is(err, os.ErrNotExist) { + if err == nil { + exitf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", flagPrivateKeyFile) + } + exitf("Error: %v", err) } - } - if dir := filepath.Dir(pubKeyFile); dir != "" && dir != "." && dir != "/" { - if err := os.MkdirAll(dir, 0o755); err != nil { - log.Fatalf("Error: %v", err) + if _, err := os.Stat(flagPublicKeyFile); !errors.Is(err, os.ErrNotExist) { + if err == nil { + exitf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", flagPublicKeyFile) + } + exitf("Error: %v", err) } } + // Generate public / private key pair publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) + } + pubKey, err := publicKey.MarshalText() + if err != nil { + exitf("Error: %v", err) } - var privateKeyBytes []byte - if unencrypted { - privateKeyBytes, err = privateKey.MarshalText() - if err != nil { - log.Fatalf("Error: %v", err) + // Marshal or encrypt private key + var privKey []byte + if flagWithoutPassword { + if privKey, err = privateKey.MarshalText(); err != nil { + exitf("Error: %v", err) } } else { var password string - if term.IsTerminal(int(os.Stdin.Fd())) { + if isTerm(os.Stdin) { fmt.Print("Please enter a password to protect the secret key.\n\n") - password = readPassword(os.Stdin, "Enter Password: ") - passwordAgain := readPassword(os.Stdin, "Enter Password (one more time): ") + password = readPassword(os.Stdin, "Password: ") + passwordAgain := readPassword(os.Stdin, "Password (one more time): ") if password != passwordAgain { - log.Fatal("Error: passwords don't match") + exit("Error: passwords don't match") } } else { - password = readPassword(os.Stdin, "Enter Password: ") + password = readPassword(os.Stdin, "Password: ") } fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") - privateKeyBytes, err = minisign.EncryptKey(password, privateKey) + privKey, err = minisign.EncryptKey(password, privateKey) if err != nil { fmt.Println() - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } fmt.Print("done\n\n") } - fileFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC - if !force { - fileFlags |= os.O_EXCL // fail if the file already exists - } - skFile, err := os.OpenFile(secKeyFile, fileFlags, 0o600) - if err != nil { - log.Fatalf("Error: %v", err) - } - defer skFile.Close() - if _, err = skFile.Write(privateKeyBytes); err != nil { - log.Fatalf("Error: %v", err) - } - - pkFile, err := os.OpenFile(pubKeyFile, fileFlags, 0o644) - if err != nil { - log.Fatalf("Error: %v", err) + // Save public and private key + if err = os.WriteFile(flagPrivateKeyFile, privKey, 0o600); err != nil { + exitf("Error: %v", err) } - defer pkFile.Close() - - rawPublicKey, _ := publicKey.MarshalText() - if _, err = pkFile.Write(rawPublicKey); err != nil { - log.Fatalf("Error: %v", err) + if err = os.WriteFile(flagPublicKeyFile, pubKey, 0o644); err != nil { + exitf("Error: %v", err) } - fmt.Printf("The secret key was saved as %s - Keep it secret!\n", secKeyFile) - fmt.Printf("The public key was saved as %s - That one can be public.\n", pubKeyFile) - fmt.Println() - fmt.Println("Files signed using this key pair can be verified with the following command:") - fmt.Println() - fmt.Printf("minisign -Vm -P %s\n", publicKey) + var b = &strings.Builder{} + fmt.Fprintf(b, "The secret key was saved as %s - Keep it secret!\n", flagPrivateKeyFile) + fmt.Fprintf(b, "The public key was saved as %s - That one can be public.\n", flagPublicKeyFile) + fmt.Fprintln(b) + fmt.Fprintln(b, "Files signed using this key pair can be verified with the following command:") + fmt.Fprintln(b) + fmt.Fprintf(b, "minisign -Vm -P %s\n", publicKey) + fmt.Print(b) } -func signFiles(secKeyFile, sigFile, untrustedComment, trustedComment string, files ...string) { - if len(files) == 0 { - log.Fatal("Error: no files to sign. Use -m to specify one or more file paths") +func signFiles() { + if len(flagFiles) == 0 { + exit("Error: no files to sign. Use -m to specify one or more file paths") } - if len(files) > 1 && sigFile != "" { - log.Fatal("Error: -x cannot be used when more than one file should be signed") - } - for _, name := range files { - stat, err := os.Stat(name) - if err != nil { - log.Fatalf("Error: %v", err) - } - if stat.IsDir() { - log.Fatalf("Error: %s is a directory", name) - } + if len(flagFiles) > 1 && flagSignatureFile != "" { + exit("Error: -x cannot be used when more than one file should be signed") } - privateKeyBytes, err := os.ReadFile(secKeyFile) + var key minisign.PrivateKey + keyBytes, err := os.ReadFile(flagPrivateKeyFile) if err != nil { - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } - - var privateKey minisign.PrivateKey - if minisign.IsEncrypted(privateKeyBytes) { - password := readPassword(os.Stdin, "Enter password: ") - + if minisign.IsEncrypted(keyBytes) { + password := readPassword(os.Stdin, "Password: ") fmt.Print("Deriving a key from the password in order to decrypt the secret key... ") - privateKey, err = minisign.DecryptKey(password, privateKeyBytes) - if err != nil { + if key, err = minisign.DecryptKey(password, keyBytes); err != nil { fmt.Println() - log.Fatalf("Error: invalid password: %v", err) + exitf("Error: invalid password: %v", err) } fmt.Print("done\n\n") - } else if err = privateKey.UnmarshalText(privateKeyBytes); err != nil { - log.Fatalf("Error: %v", err) + } else if err = key.UnmarshalText(keyBytes); err != nil { + exitf("Error: %v", err) } - if sigFile != "" { - if dir := filepath.Dir(sigFile); dir != "" && dir != "." && dir != "/" { - if err := os.MkdirAll(dir, 0o755); err != nil { - log.Fatalf("Error: %v", err) - } - } + if flagSignatureFile != "" { + mkdirs(filepath.Dir(flagSignatureFile)) } - - for _, name := range files { - var signature []byte - file, err := os.Open(name) - if err != nil { - log.Fatalf("Error: %v", err) - } - - tComment, uComment := trustedComment, untrustedComment + for _, name := range flagFiles { + tComment, uComment := flagTrustedComment, flagUntrustedComment if uComment == "" { uComment = "signature from minisign secret key" } if tComment == "" { tComment = fmt.Sprintf("timestamp:%d\tfilename:%s", time.Now().Unix(), filepath.Base(name)) } + + file, err := os.Open(name) + if err != nil { + exitf("Error: %v", err) + } + if stat, _ := file.Stat(); stat != nil && stat.IsDir() { + exitf("Error: %s is a directory", name) + } + reader := minisign.NewReader(file) - if _, err = io.Copy(io.Discard, reader); err != nil { - file.Close() - log.Fatalf("Error: %v", err) + _, err = io.Copy(io.Discard, reader) + if _ = file.Close(); err != nil { + exitf("Error: %v", err) } - signature = reader.SignWithComments(privateKey, tComment, uComment) - file.Close() - signatureFile := name + ".minisig" - if sigFile != "" { - signatureFile = sigFile + signature := reader.SignWithComments(key, tComment, uComment) + signatureFile := flagSignatureFile + if signatureFile == "" { + signatureFile = name + ".minisig" } if err = os.WriteFile(signatureFile, signature, 0o644); err != nil { - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } } } -func verifyFile(sigFile, pubFile, pubKeyString string, printOutput, quiet, prettyQuiet, requireHash bool, files ...string) { - if len(files) == 0 { - log.Fatalf("Error: no files to verify. Use -m to specify a file path") +func verifyFile() { + if len(flagFiles) == 0 { + exitf("Error: no files to verify. Use -m to specify a file path") } - if len(files) > 1 { - log.Fatalf("Error: too many files to verify. Only one file can be specified") + if len(flagFiles) > 1 { + exitf("Error: too many files to verify. Only one file can be specified") } - if sigFile == "" { - sigFile = files[0] + ".minisig" + + signatureFile := flagSignatureFile + if signatureFile == "" { + signatureFile = flagFiles[0] + ".minisig" } - var ( - publicKey minisign.PublicKey - err error - ) - if pubKeyString != "" { - if err = publicKey.UnmarshalText([]byte(pubKeyString)); err != nil { - log.Fatalf("Error: invalid public key: %v", err) + var publicKey minisign.PublicKey + if flagPublicKey != "" { + if err := publicKey.UnmarshalText([]byte(flagPublicKey)); err != nil { + exitf("Error: invalid public key: %v", err) } } else { - publicKey, err = minisign.PublicKeyFromFile(pubFile) - if err != nil { - log.Fatalf("Error: %v", err) + var err error + if publicKey, err = minisign.PublicKeyFromFile(flagPublicKeyFile); err != nil { + exitf("Error: %v", err) } } - signature, err := minisign.SignatureFromFile(sigFile) + signature, err := minisign.SignatureFromFile(signatureFile) if err != nil { - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } if signature.KeyID != publicKey.ID() { - log.Fatalf("Error: key IDs do not match. Try a different public key.\nID (public key): %X\nID (signature) : %X", publicKey.ID(), signature.KeyID) + exitf("Error: key IDs do not match. Try a different public key.\nID (public key): %X\nID (signature) : %X", publicKey.ID(), signature.KeyID) } - rawSignature, _ := signature.MarshalText() - if requireHash && signature.Algorithm != minisign.HashEdDSA { - log.Fatal("Legacy (non-prehashed) signature found") + rawSignature, err := signature.MarshalText() + if err != nil { + exitf("Error: %v", err) + } + if flagPreHash && signature.Algorithm != minisign.HashEdDSA { + exit("Legacy (non-prehashed) signature found") } - if signature.Algorithm == minisign.HashEdDSA || requireHash { - file, err := os.Open(files[0]) + if signature.Algorithm == minisign.HashEdDSA || flagPreHash { + file, err := os.Open(flagFiles[0]) if err != nil { - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } + defer file.Close() + reader := minisign.NewReader(file) if _, err = io.Copy(io.Discard, reader); err != nil { - file.Close() - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } - if !reader.Verify(publicKey, rawSignature) { - file.Close() - log.Fatal("Error: signature verification failed") + exit("Error: signature verification failed") } - if !quiet { - if !prettyQuiet { + if !flagQuiet { + if !flagPrettyQuiet { fmt.Println("Signature and comment signature verified") } fmt.Println("Trusted comment:", signature.TrustedComment) } - if printOutput { + + if flagOutput { if _, err = file.Seek(0, io.SeekStart); err != nil { - file.Close() - log.Fatalf("Error: %v", err) + exitf("Error: %v", err) } if _, err = io.Copy(os.Stdout, bufio.NewReader(file)); err != nil { - file.Close() - log.Fatalf("Error: %v", err) - } - } - file.Close() - } else { - message, err := os.ReadFile(files[0]) - if err != nil { - log.Fatalf("Error: %v", err) - } - if !minisign.Verify(publicKey, message, rawSignature) { - log.Fatal("Error: signature verification failed") - } - if !quiet { - if !prettyQuiet { - fmt.Println("Signature and comment signature verified") + exitf("Error: %v", err) } - fmt.Println("Trusted comment:", signature.TrustedComment) } - if printOutput { - os.Stdout.Write(message) + return + } + + message, err := os.ReadFile(flagFiles[0]) + if err != nil { + exitf("Error: %v", err) + } + if !minisign.Verify(publicKey, message, rawSignature) { + exit("Error: signature verification failed") + } + if !flagQuiet { + if !flagPrettyQuiet { + fmt.Println("Signature and comment signature verified") } + fmt.Println("Trusted comment:", signature.TrustedComment) + } + if flagOutput { + os.Stdout.Write(message) } } -func recreateKeyPair(secKeyFile, pubKeyFile string, force bool) { - if !force { - if _, err := os.Stat(pubKeyFile); err == nil { - log.Fatalf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", pubKeyFile) +func restorePublicKey() { + if !flagForce { + if _, err := os.Stat(flagPublicKeyFile); err == nil { + exitf("Error: %s already exists. Use -f if you really want to overwrite the existing key pair", flagPublicKeyFile) } } - if _, err := os.Stat(secKeyFile); err != nil { - log.Fatalf("Error: %v", err) - } - password := readPassword(os.Stdin, "Enter password: ") - fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") - privateKey, err := minisign.PrivateKeyFromFile(password, secKeyFile) + var privateKey minisign.PrivateKey + keyBytes, err := os.ReadFile(flagPrivateKeyFile) if err != nil { - fmt.Println() - log.Fatalf("Error: invalid password: %v", err) + exitf("Error: %v", err) + } + if minisign.IsEncrypted(keyBytes) { + password := readPassword(os.Stdin, "Password: ") + fmt.Print("Deriving a key from the password in order to decrypt the secret key... ") + if privateKey, err = minisign.DecryptKey(password, keyBytes); err != nil { + fmt.Println() + exitf("Error: invalid password: %v", err) + } + fmt.Println("done") + } else if err = privateKey.UnmarshalText(keyBytes); err != nil { + exitf("Error: %v", err) } - fmt.Println("done") - publicKey := privateKey.Public().(minisign.PublicKey) - rawPublicKey, _ := publicKey.MarshalText() - if err = os.WriteFile(pubKeyFile, rawPublicKey, 0o644); err != nil { - log.Fatalf("Error: %v", err) + publicKey, err := privateKey.Public().(minisign.PublicKey).MarshalText() + if err != nil { + exitf("Error: %v", err) + } + if err = os.WriteFile(flagPublicKeyFile, publicKey, 0o644); err != nil { + exitf("Error: %v", err) } } -func readPassword(file *os.File, message string) string { - if !term.IsTerminal(int(file.Fd())) { // If file is not a terminal read the password directly from it - p, err := bufio.NewReader(file).ReadString('\n') +func changePassword() { + keyBytes, err := os.ReadFile(flagPrivateKeyFile) + if err != nil { + exitf("Error: %v", err) + } + + // minisign always prints this message - even if the private key is not encrypted + if flagWithoutPassword { + fmt.Printf("Key encryption for [%s] is going to be removed.\n", flagPrivateKeyFile) + } + + // Unmarshal or decrypt private key + var privateKey minisign.PrivateKey + if minisign.IsEncrypted(keyBytes) { + password := readPassword(os.Stdin, "Password: ") + fmt.Print("Deriving a key from the password in order to decrypt the secret key... ") + privateKey, err = minisign.DecryptKey(password, keyBytes) if err != nil { - log.Fatalf("Error: failed to read password: %v", err) + fmt.Println() + exitf("Error: invalid password: %v", err) } - return strings.TrimSuffix(p, "\n") // ReadString contains the trailing '\n' + fmt.Print("done\n\n") + } else if err = privateKey.UnmarshalText(keyBytes); err != nil { + exitf("Error: %v", err) } - fmt.Fprint(file, message) - p, err := term.ReadPassword(int(file.Fd())) - fmt.Fprintln(file) + // Marshal or encrypt private key + if flagWithoutPassword { + if keyBytes, err = privateKey.MarshalText(); err != nil { + exitf("Error: %v", err) + } + } else { + var password string + if isTerm(os.Stdin) { + fmt.Print("Please enter a password to protect the secret key.\n\n") + password = readPassword(os.Stdin, "Password: ") + passwordAgain := readPassword(os.Stdin, "Password (one more time): ") + if password != passwordAgain { + exit("Error: passwords don't match") + } + } else { + password = readPassword(os.Stdin, "Password: ") + } - if err != nil { - log.Fatalf("Error: failed to read password: %v", err) + fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") + if keyBytes, err = minisign.EncryptKey(password, privateKey); err != nil { + fmt.Println() + exitf("Error: %v", err) + } } - return string(p) + + // Save private key. Use rename to prevent corrupting a private on write failure. + if err = os.WriteFile(flagPrivateKeyFile+".tmp", keyBytes, 0o600); err != nil { + exitf("Error: %v", err) + } + if err = os.Rename(flagPrivateKeyFile+".tmp", flagPrivateKeyFile); err != nil { + exitf("Error: %v", err) + } + if flagWithoutPassword { + fmt.Println("Password removed.") // Again, minisign always prints this message + } else { + fmt.Println("done\n\nPassword updated.") + } +} + +type filenames []string + +var _ flag.Value = (*filenames)(nil) // compiler check + +func (f *filenames) String() string { return fmt.Sprint(*f) } + +func (f *filenames) Set(value string) error { + *f = append(*f, value) + return nil } +// expandFlags expands args such that the flag package can parse them. +// For example, the arguments '-Voqm foo.txt bar.txt' are expanded to +// '-V -o -q -m foo.txt bar.txt'. func expandFlags(args []string) []string { expArgs := make([]string, 0, len(args)) for _, arg := range args { @@ -445,13 +489,77 @@ func expandFlags(args []string) []string { return expArgs } -type multiFlag []string +// homedir returns the platform's user home directory. +// If no home directory can be detected, it aborts the +// program. +func homedir() string { + home, err := os.UserHomeDir() + if err != nil { + exitf("Error: failed to detect home directory: %v", err) + } + return home +} + +// mkdirs creates the directory p, and any non-existing +// parent directories, unless p is empty, "." or a single +// path separator. +func mkdirs(p string) { + if p == "" { + return + } + if len(p) > 1 || (p[0] != '.' && !os.IsPathSeparator(p[0])) { + if err := os.Mkdir(p, 0o755); !errors.Is(err, os.ErrExist) { + if errors.Is(err, os.ErrNotExist) { + err = os.MkdirAll(p, 0o755) + } + if err != nil { + exitf("Error: %v", err) + } + } + } +} + +// readPassword reads a password from the file descriptor. +// If file is a terminal, it prints the message before waiting +// for the user to enter the password. +func readPassword(file *os.File, message string) string { + if !isTerm(file) { // If file is not a terminal read the password directly from it + p, err := bufio.NewReader(file).ReadString('\n') + if err != nil { + exitf("Error: failed to read password: %v", err) + } + + // ReadString returns a string with the trailing newline + if strings.HasSuffix(p, "\r\n") { + return strings.TrimSuffix(p, "\r\n") // windows + } + return strings.TrimSuffix(p, "\n") // unix + } -var _ flag.Value = (*multiFlag)(nil) // compiler check + fmt.Fprint(file, message) + p, err := term.ReadPassword(int(file.Fd())) + fmt.Fprintln(file) -func (f *multiFlag) String() string { return fmt.Sprint(*f) } + if err != nil { + exitf("Error: failed to read password: %v", err) + } + return string(p) +} -func (f *multiFlag) Set(value string) error { - *f = append(*f, value) - return nil +// isTerm reports whether fd is a terminal +func isTerm(fd *os.File) bool { return term.IsTerminal(int(fd.Fd())) } + +// exit formats and prints its args to stderr before exiting +// the program. +func exit(args ...any) { + fmt.Println() + fmt.Fprintln(os.Stderr, args...) + os.Exit(1) +} + +// exitf formats and prints its args to stderr before exiting +// the program. +func exitf(format string, args ...any) { + fmt.Fprintln(os.Stderr, fmt.Sprintf(format, args...)) + os.Exit(1) } diff --git a/internal/testdata/unencrypted-0.key b/internal/testdata/unencrypted-0.key new file mode 100644 index 0000000..d1fcc14 --- /dev/null +++ b/internal/testdata/unencrypted-0.key @@ -0,0 +1,2 @@ +untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/internal/testdata/unencrypted.key b/internal/testdata/unencrypted-1.key similarity index 50% rename from internal/testdata/unencrypted.key rename to internal/testdata/unencrypted-1.key index 2b92a65..913ddbb 100644 --- a/internal/testdata/unencrypted.key +++ b/internal/testdata/unencrypted-1.key @@ -1,5 +1,5 @@ -untrusted comment: minisign encrypted secret key -RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - untrusted comment: minisign encrypted secret key RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb/yydu4x5dcvbgaLZRtY5v8wFvgzMkvKyALUXUWcT+bvaqFvuvkUyUfMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + diff --git a/private.go b/private.go index 676d722..fd77f4d 100644 --- a/private.go +++ b/private.go @@ -5,7 +5,6 @@ package minisign import ( - "bytes" "crypto" "crypto/ed25519" "crypto/rand" @@ -144,16 +143,10 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { } var ( - empty [32]byte // For checking that the salt/tag are empty - - kType = binary.LittleEndian.Uint16(b) - kdf = binary.LittleEndian.Uint16(b[2:]) - hType = binary.LittleEndian.Uint16(b[4:]) - salt = b[6:38] - scryptOps = binary.LittleEndian.Uint64(b[38:]) - scryptMem = binary.LittleEndian.Uint64(b[46:]) - key = b[54:126] - tag = b[126:privateKeySize] + kType = binary.LittleEndian.Uint16(b) + kdf = binary.LittleEndian.Uint16(b[2:]) + hType = binary.LittleEndian.Uint16(b[4:]) + key = b[54:126] ) if kType != EdDSA { return fmt.Errorf("minisign: invalid private key: invalid key type '%d'", kType) @@ -167,18 +160,6 @@ func (p *PrivateKey) UnmarshalText(text []byte) error { if hType != algorithmBlake2b { return fmt.Errorf("minisign: invalid private key: invalid hash type '%d'", hType) } - if !bytes.Equal(salt, empty[:]) { - return errors.New("minisign: invalid private key: salt is not empty") - } - if scryptOps != 0 { - return errors.New("minisign: invalid private key: scrypt cost parameter is not zero") - } - if scryptMem != 0 { - return errors.New("minisign: invalid private key: scrypt mem parameter is not zero") - } - if !bytes.Equal(tag, empty[:]) { - return errors.New("minisign: invalid private key: salt is not empty") - } p.id = binary.LittleEndian.Uint64(key) copy(p.bytes[:], key[8:]) diff --git a/private_test.go b/private_test.go index 3a2f7de..b1caf30 100644 --- a/private_test.go +++ b/private_test.go @@ -13,32 +13,31 @@ import ( ) var marshalPrivateKeyTests = []struct { + File string ID uint64 Bytes []byte }{ { + File: "./internal/testdata/unencrypted-0.key", ID: htoi("3728470A8118E56E"), Bytes: b64("JpjEI/XIKqIVl99tT611AxXwlVjlw2afJC8Nv6o7uuipyNvC3DmgO2csDT+bw1bZR3ss4rd5cXqoq0uftlCJqw=="), }, { + File: "./internal/testdata/unencrypted-1.key", ID: htoi("D7E531EE76B2FC6F"), Bytes: b64("L24Gi2UbWOb/MBb4MzJLysgC1F1FnE/m72qhb7r5FMlHzHe6M6mCLPMzmj6ln+hI51kqpDqTkIg9VCaToAhZtA=="), }, } func TestPrivateKey_Marshal(t *testing.T) { - raw, err := os.ReadFile("./internal/testdata/unencrypted.key") - if err != nil { - t.Fatalf("Failed to read private key: %v", err) - } - raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) - raw = bytes.TrimSuffix(raw, []byte{'\n'}) - - keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline - if len(keys) != len(marshalPrivateKeyTests) { - t.Fatalf("Test vectors don't match: got %d - want %d", len(marshalPrivateKeyTests), len(keys)) - } for i, test := range marshalPrivateKeyTests { + raw, err := os.ReadFile(test.File) + if err != nil { + t.Fatalf("Failed to read private key: %v", err) + } + raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) + raw = bytes.TrimRight(raw, "\n") + key := PrivateKey{ id: test.ID, } @@ -48,27 +47,27 @@ func TestPrivateKey_Marshal(t *testing.T) { if err != nil { t.Fatalf("Test %d: failed to marshal private key: %v", i, err) } - if !bytes.Equal(text, keys[i]) { - t.Log(len(text), len(keys[i])) - t.Log(string(keys[i][len(keys[i])-1])) - t.Fatalf("Test %d: failed to marshal private key:\nGot: %v\nWant: %v\n", i, text, keys[i]) + if !bytes.Equal(text, raw) { + t.Fatalf("Test %d: failed to marshal private key:\nGot: %v\nWant: %v\n", i, text, raw) } } } +var unmarshalPrivateKeyTests = []string{ + "./internal/testdata/unencrypted-0.key", + "./internal/testdata/unencrypted-1.key", +} + func TestPrivateKey_Unmarshal(t *testing.T) { - raw, err := os.ReadFile("./internal/testdata/unencrypted.key") - if err != nil { - t.Fatalf("Failed to read private key: %v", err) - } - raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'}) - raw = bytes.TrimSuffix(raw, []byte{'\n'}) + for i, file := range unmarshalPrivateKeyTests { + raw, err := os.ReadFile(file) + if err != nil { + t.Fatalf("Test %d: failed to read private key: %v", i, err) + } - keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline - for _, k := range keys { var key PrivateKey - if err := key.UnmarshalText(k); err != nil { - t.Fatalf("Failed to unmarshal private key: %v\nPrivate key:\n%s", err, string(k)) + if err := key.UnmarshalText(raw); err != nil { + t.Fatalf("Test %d: failed to unmarshal private key: %v\nPrivate key:\n%s", i, err, string(raw)) } // Print test vector for marshaling: From 54b14628bfa0c145e161e830cf1902de8b67204b Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Sat, 18 May 2024 14:30:33 +0200 Subject: [PATCH 7/9] Update README.md --- README.md | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4acf8f5..dab34fb 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![latest](https://badgen.net/github/tag/aead/minisign)](https://github.com/aead/minisign/releases/latest) # minisign + minisign is a dead simple tool to sign files and verify signatures. ``` @@ -37,13 +38,16 @@ This is a Go implementation of the [original C implementation](https://github.co ``` Usage: - minisign -G [-p ] [-s ] + minisign -G [-p ] [-s ] [-W] + minisign -R [-s ] [-p ] + minisign -C [-s ] [-W] minisign -S [-x ] [-s ] [-c ] [-t ] -m ... minisign -V [-H] [-x ] [-p | -P ] [-o] [-q | -Q ] -m - minisign -R [-s ] [-p ] - + Options: - -G Generate a new public/secret key pair. + -G Generate a new public/secret key pair. + -R Re-create a public key file from a secret key. + -C Change or remove the password of the secret key. -S Sign files with a secret key. -V Verify files with a public key. -m The file to sign or verify. @@ -52,19 +56,24 @@ Options: -p Public key file (default: ./minisign.pub) -P Public key as base64 string -s Secret key file (default: $HOME/.minisign/minisign.key) + -W Do not encrypt/decrypt the secret key with a password. -x Signature file (default: .minisig) -c Add a one-line untrusted comment. -t Add a one-line trusted comment. -q Quiet mode. Suppress output. -Q Pretty quiet mode. Combined with -V, only print the trusted comment. - -R Re-create a public key file from a secret key. -f Combined with -G or -R, overwrite any existing public/secret key pair. -v Print version information. ``` ## Installation -On windows, linux and macOS, you can use the pre-built binaries: +With an up-to-date Go toolchain: +``` +go install aead.dev/minisign/cmd/minisign@latest +``` + +On windows, linux and macOS, you can also use the pre-built binaries: | OS | ARCH | Latest Release | |:---------:|:-------:|:-----------------------------------------------------------------------------------------------------------------------| | Linux | amd64 | [minisign-linux-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-linux-amd64.tar.gz) | @@ -73,10 +82,15 @@ On windows, linux and macOS, you can use the pre-built binaries: | MacOS | amd64 | [minisign-darwin-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-darwin-amd64.tar.gz) | | Windows | amd64 | [minisign-windows-amd64.zip](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-windows-amd64.zip) | -If your system has [Go1.16+](https://golang.org/dl/), you can build from source: -``` -git clone https://aead.dev/minisign && cd minisign && make build -``` +From source: +1. Clone the repository + ``` + git clone https://aead.dev/minisign && cd minisign + ``` +2. Build the binary + ``` + make build + ``` ## Library @@ -99,14 +113,14 @@ import ( func main() { var message = []byte("Hello World!") - public, private, err := minisign.GenerateKey(rand.Reader) + publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { log.Fatalln(err) } - signature := minisign.Sign(private, message) + signature := minisign.Sign(privateKey, message) - if !minisign.Verify(public, message, signature) { + if !minisign.Verify(publicKey, message, signature) { log.Fatalln("signature verification failed") } log.Println(string(message)) From 989464fbe0f725116fff14a3be72862d32400ad2 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Sun, 19 May 2024 12:07:40 +0200 Subject: [PATCH 8/9] adjust private key tests This commit adjusts and simplifies the private key tests slightly. Signed-off-by: Andreas Auernhammer --- cmd/minisign/minisign.go | 1 - ...rypted-0.key => minisign-nopassword-0.key} | 0 ...rypted-1.key => minisign-nopassword-1.key} | 0 private_test.go | 44 ++++++++++++------- 4 files changed, 27 insertions(+), 18 deletions(-) rename internal/testdata/{unencrypted-0.key => minisign-nopassword-0.key} (100%) rename internal/testdata/{unencrypted-1.key => minisign-nopassword-1.key} (100%) diff --git a/cmd/minisign/minisign.go b/cmd/minisign/minisign.go index 27ee50c..cc953e3 100644 --- a/cmd/minisign/minisign.go +++ b/cmd/minisign/minisign.go @@ -552,7 +552,6 @@ func isTerm(fd *os.File) bool { return term.IsTerminal(int(fd.Fd())) } // exit formats and prints its args to stderr before exiting // the program. func exit(args ...any) { - fmt.Println() fmt.Fprintln(os.Stderr, args...) os.Exit(1) } diff --git a/internal/testdata/unencrypted-0.key b/internal/testdata/minisign-nopassword-0.key similarity index 100% rename from internal/testdata/unencrypted-0.key rename to internal/testdata/minisign-nopassword-0.key diff --git a/internal/testdata/unencrypted-1.key b/internal/testdata/minisign-nopassword-1.key similarity index 100% rename from internal/testdata/unencrypted-1.key rename to internal/testdata/minisign-nopassword-1.key diff --git a/private_test.go b/private_test.go index b1caf30..854cfd1 100644 --- a/private_test.go +++ b/private_test.go @@ -8,7 +8,6 @@ import ( "bytes" "encoding/base64" "os" - "strconv" "testing" ) @@ -18,13 +17,13 @@ var marshalPrivateKeyTests = []struct { Bytes []byte }{ { - File: "./internal/testdata/unencrypted-0.key", - ID: htoi("3728470A8118E56E"), + File: "./internal/testdata/minisign-nopassword-0.key", + ID: 0x3728470A8118E56E, Bytes: b64("JpjEI/XIKqIVl99tT611AxXwlVjlw2afJC8Nv6o7uuipyNvC3DmgO2csDT+bw1bZR3ss4rd5cXqoq0uftlCJqw=="), }, { - File: "./internal/testdata/unencrypted-1.key", - ID: htoi("D7E531EE76B2FC6F"), + File: "./internal/testdata/minisign-nopassword-1.key", + ID: 0xD7E531EE76B2FC6F, Bytes: b64("L24Gi2UbWOb/MBb4MzJLysgC1F1FnE/m72qhb7r5FMlHzHe6M6mCLPMzmj6ln+hI51kqpDqTkIg9VCaToAhZtA=="), }, } @@ -53,14 +52,26 @@ func TestPrivateKey_Marshal(t *testing.T) { } } -var unmarshalPrivateKeyTests = []string{ - "./internal/testdata/unencrypted-0.key", - "./internal/testdata/unencrypted-1.key", +var unmarshalPrivateKeyTests = []struct { + File string + ID uint64 + Bytes []byte +}{ + { + File: "./internal/testdata/minisign-nopassword-0.key", + ID: 0x3728470A8118E56E, + Bytes: b64("JpjEI/XIKqIVl99tT611AxXwlVjlw2afJC8Nv6o7uuipyNvC3DmgO2csDT+bw1bZR3ss4rd5cXqoq0uftlCJqw=="), + }, + { + File: "./internal/testdata/minisign-nopassword-1.key", + ID: 0xD7E531EE76B2FC6F, + Bytes: b64("L24Gi2UbWOb/MBb4MzJLysgC1F1FnE/m72qhb7r5FMlHzHe6M6mCLPMzmj6ln+hI51kqpDqTkIg9VCaToAhZtA=="), + }, } func TestPrivateKey_Unmarshal(t *testing.T) { - for i, file := range unmarshalPrivateKeyTests { - raw, err := os.ReadFile(file) + for i, test := range unmarshalPrivateKeyTests { + raw, err := os.ReadFile(test.File) if err != nil { t.Fatalf("Test %d: failed to read private key: %v", i, err) } @@ -72,15 +83,14 @@ func TestPrivateKey_Unmarshal(t *testing.T) { // Print test vector for marshaling: // t.Logf("\n{\n\tID: htoi(\"%X\"),\n\tBytes: b64(\"%s\"),\n}", key.id, base64.StdEncoding.EncodeToString(key.bytes[:])) - } -} -func htoi(s string) uint64 { - i, err := strconv.ParseUint(s, 16, 64) - if err != nil { - panic(err) + if key.ID() != test.ID { + t.Fatalf("Test %d: ID mismatch: got '%x' - want '%x'", i, key.ID(), test.ID) + } + if !bytes.Equal(key.bytes[:], test.Bytes) { + t.Fatalf("Test %d: private key mismatch: got '%x' - want '%x'", i, key.bytes, test.Bytes) + } } - return i } func b64(s string) []byte { From 41e136626bd44c4a4a0540d8c6b3c863f71f32a7 Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Sun, 19 May 2024 12:21:42 +0200 Subject: [PATCH 9/9] update version to v0.3.0 Signed-off-by: Andreas Auernhammer --- Makefile | 9 ++++----- README.md | 9 ++++----- cmd/minisign/minisign.go | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 7d1dbbf..c1cae93 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ ifneq ($(shell go env GOBIN),) else GOBIN := $(shell $(go env GOPATH)/bin) endif -VERSION := v0.2.1 .PHONY: build check release test lint update-tools @@ -20,22 +19,22 @@ ifneq ($(shell git status -s) , ) @(echo "Repository contains modified files." && exit 1) else @echo -n Building minisign ${VERSION} for linux/amd64... - @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ./minisign ./cmd/minisign @tar -czf minisign-linux-amd64.tar.gz ./minisign ./LICENSE ./README.md @echo " DONE." @echo -n Building minisign ${VERSION} for linux/arm64... - @GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ./minisign ./cmd/minisign @tar -czf minisign-linux-arm64.tar.gz ./minisign ./LICENSE ./README.md @echo " DONE." @echo -n Building minisign ${VERSION} for darwin/arm64... - @GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ./minisign ./cmd/minisign @tar -czf minisign-darwin-arm64.tar.gz ./minisign ./LICENSE ./README.md @echo " DONE." @echo -n Building minisign ${VERSION} for windows/amd64... - @GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}'" -o ./minisign ./cmd/minisign + @GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ./minisign ./cmd/minisign @zip -q minisign-windows-amd64.zip ./minisign ./LICENSE ./README.md @echo " DONE." diff --git a/README.md b/README.md index dab34fb..ade9f31 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,10 @@ go install aead.dev/minisign/cmd/minisign@latest On windows, linux and macOS, you can also use the pre-built binaries: | OS | ARCH | Latest Release | |:---------:|:-------:|:-----------------------------------------------------------------------------------------------------------------------| -| Linux | amd64 | [minisign-linux-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-linux-amd64.tar.gz) | -| Linux | arm64 | [minisign-linux-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-linux-arm64.tar.gz) | -| MacOS | arm64 | [minisign-darwin-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-darwin-arm64.tar.gz) | -| MacOS | amd64 | [minisign-darwin-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-darwin-amd64.tar.gz) | -| Windows | amd64 | [minisign-windows-amd64.zip](https://github.com/aead/minisign/releases/download/v0.2.1/minisign-windows-amd64.zip) | +| Linux | amd64 | [minisign-linux-amd64.tar.gz](https://github.com/aead/minisign/releases/download/v0.3.0/minisign-linux-amd64.tar.gz) | +| Linux | arm64 | [minisign-linux-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.3.0/minisign-linux-arm64.tar.gz) | +| MacOS | arm64 | [minisign-darwin-arm64.tar.gz](https://github.com/aead/minisign/releases/download/v0.3.0/minisign-darwin-arm64.tar.gz) | +| Windows | amd64 | [minisign-windows-amd64.zip](https://github.com/aead/minisign/releases/download/v0.3.0/minisign-windows-amd64.zip) | From source: 1. Clone the repository diff --git a/cmd/minisign/minisign.go b/cmd/minisign/minisign.go index cc953e3..013b8bf 100644 --- a/cmd/minisign/minisign.go +++ b/cmd/minisign/minisign.go @@ -21,7 +21,7 @@ import ( "golang.org/x/term" ) -const version = "v0.2.1" +const version = "v0.3.0" const usage = `Usage: minisign -G [-p ] [-s ] [-W]