pax_global_header00006660000000000000000000000064146425214220014514gustar00rootroot0000000000000052 comment=330b6adadf869affef5b562c8d7c1e7fac39bf0a golang-github-aead-minisign-0.3.0/000077500000000000000000000000001464252142200167465ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/.github/000077500000000000000000000000001464252142200203065ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/.github/workflows/000077500000000000000000000000001464252142200223435ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/.github/workflows/go.yml000066400000000000000000000040161464252142200234740ustar00rootroot00000000000000name: Go on: pull_request: branches: - main push: branches: - main jobs: build: name: Build ${{ 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 - name: Build and Lint run: | go build ./... go vet ./... lint: name: Lint runs-on: ubuntu-latest steps: - name: "Set up Go" uses: actions/setup-go@v3 with: go-version: 1.22.3 - name: Check out code uses: actions/checkout@v3 - name: Lint uses: golangci/golangci-lint-action@v3 with: version: latest args: --config ./.golangci.yml --timeout=2m test: name: Text ${{ matrix.os }} | ${{ matrix.go-version }} runs-on: ${{ matrix.os }} strategy: matrix: 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 }} uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Test on ${{ matrix.os }} run: | go test ./... vulncheck: 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 - name: Get govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest shell: bash - name: Run govulncheck run: govulncheck ./... shell: bash golang-github-aead-minisign-0.3.0/.gitignore000066400000000000000000000004151464252142200207360ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ golang-github-aead-minisign-0.3.0/.golangci.yml000066400000000000000000000011361464252142200213330ustar00rootroot00000000000000linters-settings: misspell: locale: US staticcheck: checks: ["all", "-SA1019"] linters: disable-all: true enable: - durationcheck - gocritic - gofmt - goimports - gomodguard - govet - ineffassign - misspell - revive - staticcheck - tenv - typecheck - unconvert - unused issues: exclude-use-default: false exclude: - "package-comments: should have a package comment" - "exitAfterDefer:" - "captLocal:" service: golangci-lint-version: 1.57.2 # use the fixed version to not introduce new linters unexpectedly golang-github-aead-minisign-0.3.0/LICENSE000066400000000000000000000020641464252142200177550ustar00rootroot00000000000000MIT License Copyright (c) 2021 Andreas Auernhammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-aead-minisign-0.3.0/Makefile000066400000000000000000000034061464252142200204110ustar00rootroot00000000000000ifneq ($(shell go env GOBIN),) GOBIN := $(shell go env GOBIN) else GOBIN := $(shell $(go env GOPATH)/bin) endif .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" -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" -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" -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" -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 golang-github-aead-minisign-0.3.0/README.md000066400000000000000000000113511464252142200202260ustar00rootroot00000000000000[![Go Reference](https://pkg.go.dev/badge/aead.dev/minisign.svg)](https://pkg.go.dev/aead.dev/minisign) ![Github CI](https://github.com/aead/minisign/actions/workflows/go.yml/badge.svg?branch=main) [![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. ``` $ minisign -G Please enter a password to protect the secret key. Enter Password: Enter Password (one more time): Deriving a key from the password in order to encrypt the secret key... done The secret key was saved as ~/.minisign/minisign.key - Keep it secret! The public key was saved as minisign.pub - That one can be public. Files signed using this key pair can be verified with the following command: minisign -Vm -P RWSYKA736yqh+JrZ7cRDdWgck/WKtwW9ATBFmk8pQ1lHeUKXtV6uJ7Fu ``` ``` $ minisign -Sm message.txt Enter password: Deriving a key from the password in order to decrypt the secret key... done ``` ``` $ minisign -Vm message.txt Signature and comment signature verified Trusted comment: timestamp:1614718943 filename:message.txt ``` This is a Go implementation of the [original C implementation](https://github.com/jedisct1/minisign) by [Frank Denis](https://github.com/jedisct1). ## 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 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. -o Combined with -V, output the file after verification. -H Combined with -V, require a signature over a pre-hashed file. -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. -f Combined with -G or -R, overwrite any existing public/secret key pair. -v Print version information. ``` ## Installation 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.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 ``` git clone https://aead.dev/minisign && cd minisign ``` 2. Build the binary ``` make build ``` ## Library ```Go import "aead.dev/minisign" ``` The following example generates a minisign public/private key pair, signs a message and verifies the message signature. ```Go package main import ( "crypto/rand" "log" "aead.dev/minisign" ) func main() { var message = []byte("Hello World!") publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { log.Fatalln(err) } signature := minisign.Sign(privateKey, message) if !minisign.Verify(publicKey, message, signature) { log.Fatalln("signature verification failed") } log.Println(string(message)) } ``` For more examples visit the package [documentation](https://pkg.go.dev/aead.dev/minisign). golang-github-aead-minisign-0.3.0/cmd/000077500000000000000000000000001464252142200175115ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/cmd/minisign/000077500000000000000000000000001464252142200213265ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/cmd/minisign/minisign.go000066400000000000000000000414561464252142200235040ustar00rootroot00000000000000// 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 main import ( "bufio" "crypto/rand" "errors" "flag" "fmt" "io" "os" "path/filepath" "runtime" "strings" "time" "aead.dev/minisign" "golang.org/x/term" ) const version = "v0.3.0" 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 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. -o Combined with -V, output the file after verification. -H Combined with -V, require a signature over a pre-hashed file. -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. -f Combined with -G or -R, overwrite any existing public/secret key pair. -v Print version information. ` 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() { flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } 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 flagVersion { fmt.Printf("minisign %s on %s-%s\n", version, runtime.GOOS, runtime.GOARCH) return } switch { case flagKeyGen: generateKeyPair() case flagRestore: restorePublicKey() case flagChangePassword: changePassword() case flagSign: signFiles() case flagVerify: verifyFile() default: flag.Usage() os.Exit(1) } } func generateKeyPair() { // Create private and public key parent directories mkdirs(filepath.Dir(flagPrivateKeyFile)) mkdirs(filepath.Dir(flagPublicKeyFile)) // 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 _, 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 { exitf("Error: %v", err) } pubKey, err := publicKey.MarshalText() if err != nil { exitf("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 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: ") } fmt.Print("Deriving a key from the password in order to encrypt the secret key... ") privKey, err = minisign.EncryptKey(password, privateKey) if err != nil { fmt.Println() exitf("Error: %v", err) } fmt.Print("done\n\n") } // Save public and private key if err = os.WriteFile(flagPrivateKeyFile, privKey, 0o600); err != nil { exitf("Error: %v", err) } if err = os.WriteFile(flagPublicKeyFile, pubKey, 0o644); err != nil { exitf("Error: %v", err) } 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() { if len(flagFiles) == 0 { exit("Error: no files to sign. Use -m to specify one or more file paths") } if len(flagFiles) > 1 && flagSignatureFile != "" { exit("Error: -x cannot be used when more than one file should be signed") } var key minisign.PrivateKey keyBytes, err := os.ReadFile(flagPrivateKeyFile) if err != nil { 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 key, err = minisign.DecryptKey(password, keyBytes); err != nil { fmt.Println() exitf("Error: invalid password: %v", err) } fmt.Print("done\n\n") } else if err = key.UnmarshalText(keyBytes); err != nil { exitf("Error: %v", err) } if flagSignatureFile != "" { mkdirs(filepath.Dir(flagSignatureFile)) } 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) _, err = io.Copy(io.Discard, reader) if _ = file.Close(); err != nil { exitf("Error: %v", err) } signature := reader.SignWithComments(key, tComment, uComment) signatureFile := flagSignatureFile if signatureFile == "" { signatureFile = name + ".minisig" } if err = os.WriteFile(signatureFile, signature, 0o644); err != nil { exitf("Error: %v", err) } } } func verifyFile() { if len(flagFiles) == 0 { exitf("Error: no files to verify. Use -m to specify a file path") } if len(flagFiles) > 1 { exitf("Error: too many files to verify. Only one file can be specified") } signatureFile := flagSignatureFile if signatureFile == "" { signatureFile = flagFiles[0] + ".minisig" } var publicKey minisign.PublicKey if flagPublicKey != "" { if err := publicKey.UnmarshalText([]byte(flagPublicKey)); err != nil { exitf("Error: invalid public key: %v", err) } } else { var err error if publicKey, err = minisign.PublicKeyFromFile(flagPublicKeyFile); err != nil { exitf("Error: %v", err) } } signature, err := minisign.SignatureFromFile(signatureFile) if err != nil { exitf("Error: %v", err) } if signature.KeyID != publicKey.ID() { exitf("Error: key IDs do not match. Try a different public key.\nID (public key): %X\nID (signature) : %X", publicKey.ID(), signature.KeyID) } 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 || flagPreHash { file, err := os.Open(flagFiles[0]) if err != nil { exitf("Error: %v", err) } defer file.Close() reader := minisign.NewReader(file) if _, err = io.Copy(io.Discard, reader); err != nil { exitf("Error: %v", err) } if !reader.Verify(publicKey, 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 { if _, err = file.Seek(0, io.SeekStart); err != nil { exitf("Error: %v", err) } if _, err = io.Copy(os.Stdout, bufio.NewReader(file)); err != nil { exitf("Error: %v", err) } } 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 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) } } var privateKey minisign.PrivateKey keyBytes, err := os.ReadFile(flagPrivateKeyFile) if err != nil { 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) } 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 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 { fmt.Println() exitf("Error: invalid password: %v", err) } fmt.Print("done\n\n") } else if err = privateKey.UnmarshalText(keyBytes); err != nil { exitf("Error: %v", err) } // 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: ") } 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) } } // 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 { if !strings.HasPrefix(arg, "-") { expArgs = append(expArgs, arg) continue } if len(arg) > 2 { expArgs = append(expArgs, arg[:2]) for _, a := range arg[2:] { expArgs = append(expArgs, "-"+string(a)) } } else { expArgs = append(expArgs, arg) } } return expArgs } // 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 } fmt.Fprint(file, message) p, err := term.ReadPassword(int(file.Fd())) fmt.Fprintln(file) if err != nil { exitf("Error: failed to read password: %v", err) } return string(p) } // 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.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) } golang-github-aead-minisign-0.3.0/example_test.go000066400000000000000000000131071464252142200217710ustar00rootroot00000000000000// 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_test import ( "crypto/rand" "fmt" "io" "strconv" "strings" "aead.dev/minisign" ) func ExampleGenerateKey() { // Generate a new minisign private / public key pair. publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { panic(err) // TODO: error handling } // Sign a message with the private key message := []byte("Hello Gopher!") signature := minisign.Sign(privateKey, message) // Verify the signature with the public key and // print the message if the signature is valid. if minisign.Verify(publicKey, message, signature) { fmt.Println(string(message)) } // Output: Hello Gopher! } func ExampleEncryptKey() { // Generate a new minisign private / public key pair. // We don't care about the public key in this example. _, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { panic(err) // TODO: error handling } const password = "correct horse battery staple" // Encrypt the private key with the password encryptedKey, err := minisign.EncryptKey(password, privateKey) if err != nil { panic(err) // TODO: error handling } // Then, decrypt the encrypted key with the password again decryptedKey, err := minisign.DecryptKey(password, encryptedKey) if err != nil { panic(err) // TODO: error handling } // Now, both private keys should be identical fmt.Println(privateKey.Equal(decryptedKey)) // Output: true } func ExampleDecryptKey() { const ( rawPrivateKey = "RWRTY0IyorAWr/1gdweGki6ua7GpmoPqS+7rMBSmBy6hedA53dAAABAAAAAAAAAAAAIAAAAAwfmyB6qIIW2eGNiQaFzgs1oi52iN8cRHBPRupc9TVdfAeJvlPdvzu3TfA2DHTW2PZi98uihcr5sEB5fefFml2d0xBk72ZOGNJpOTsn95eHgEH/qUfzQZ018JfiVwWf8pNpdgNFX8ROs=" password = "correct horse battery staple" ) // Decrypt the raw private key with the password privateKey, err := minisign.DecryptKey(password, []byte(rawPrivateKey)) if err != nil { panic(err) // TODO: error handling } // Print the key ID as upper-case hex string fmt.Println("Private Key", strings.ToUpper(strconv.FormatUint(privateKey.ID(), 16))) // Output: Private Key A345BDA18A33D06 } func ExampleSign() { const ( rawPrivateKey = "RWRTY0IyorAWr/1gdweGki6ua7GpmoPqS+7rMBSmBy6hedA53dAAABAAAAAAAAAAAAIAAAAAwfmyB6qIIW2eGNiQaFzgs1oi52iN8cRHBPRupc9TVdfAeJvlPdvzu3TfA2DHTW2PZi98uihcr5sEB5fefFml2d0xBk72ZOGNJpOTsn95eHgEH/qUfzQZ018JfiVwWf8pNpdgNFX8ROs=" password = "correct horse battery staple" ) // Decrypt the raw private key with the password privateKey, err := minisign.DecryptKey(password, []byte(rawPrivateKey)) if err != nil { panic(err) // TODO: error handling } // Sign a message with the private key message := []byte("Hello Gopher!") signature := minisign.Sign(privateKey, message) fmt.Println(string(signature)) } func ExampleSignWithComments() { const ( rawPrivateKey = "RWRTY0IyorAWr/1gdweGki6ua7GpmoPqS+7rMBSmBy6hedA53dAAABAAAAAAAAAAAAIAAAAAwfmyB6qIIW2eGNiQaFzgs1oi52iN8cRHBPRupc9TVdfAeJvlPdvzu3TfA2DHTW2PZi98uihcr5sEB5fefFml2d0xBk72ZOGNJpOTsn95eHgEH/qUfzQZ018JfiVwWf8pNpdgNFX8ROs=" password = "correct horse battery staple" ) // Decrypt the raw private key with the password privateKey, err := minisign.DecryptKey(password, []byte(rawPrivateKey)) if err != nil { panic(err) // TODO: error handling } // Sign a message with comments with the private key const ( trustedComment = "This comment is signed and can be trusted" untrustedComment = "This comment is not signed and just informational" ) message := []byte("Hello Gopher!") signature := minisign.SignWithComments(privateKey, message, trustedComment, untrustedComment) fmt.Println(string(signature)) // Output: untrusted comment: This comment is not signed and just informational // RWQGPaMY2ls0CmMflCAP5J/MpaXmt+3+UoT1vRSPRjXO6w0KNtpkcQe3TxQ35kAwhjFVB6CEYYrHZmMvWjXRutefRHicRUiAJwQ= // trusted comment: This comment is signed and can be trusted // /jXXGSI/q3MhrZ5PKzL221/qC+JFVpgilf9su6AcTtMffw+9ShYt5LjU2RG1M/EspIoEv4xxK/36TeCQBgHbBw== } func ExampleVerify() { const rawPublicKey = "RWQGPaMY2ls0CkF/83ls7D+IU25w3jeYczwo3s451zDlnrJJwOdt2ro8" var ( message = []byte("Hello Gopher!") signature = []byte(`untrusted comment: signature from private key: A345BDA18A33D06 RWQGPaMY2ls0CmMflCAP5J/MpaXmt+3+UoT1vRSPRjXO6w0KNtpkcQe3TxQ35kAwhjFVB6CEYYrHZmMvWjXRutefRHicRUiAJwQ= trusted comment: timestamp:1600100266 2x/lxCqL+PHoT4I9Wc8PHmoNBtohgmFdWwPBON55Y2P0ttpBHgr4OFldr/Hq7nDcBGt5SBs2XjtMnxjVs6byBg==`) ) var publicKey minisign.PublicKey if err := publicKey.UnmarshalText([]byte(rawPublicKey)); err != nil { panic(err) // TODO: error handling } if minisign.Verify(publicKey, message, signature) { fmt.Println(string(message)) } // Output: Hello Gopher! } func ExampleReader() { // Generate a new minisign public / private key pair. publicKey, privateKey, err := minisign.GenerateKey(rand.Reader) if err != nil { panic(err) // TODO: error handling } const Message = "Hello Gopher!" // Sign a data stream after processing it. (Here, we just discard it) reader := minisign.NewReader(strings.NewReader(Message)) if _, err := io.Copy(io.Discard, reader); err != nil { panic(err) // TODO: error handling } signature := reader.Sign(privateKey) // Read a data stream and then verify its authenticity with // the public key. reader = minisign.NewReader(strings.NewReader(Message)) message, err := io.ReadAll(reader) if err != nil { panic(err) // TODO: error handling } if reader.Verify(publicKey, signature) { fmt.Println(string(message)) } // Output: Hello Gopher! } golang-github-aead-minisign-0.3.0/go.mod000066400000000000000000000002251464252142200200530ustar00rootroot00000000000000module aead.dev/minisign go 1.21 require ( golang.org/x/crypto v0.13.0 golang.org/x/term v0.12.0 ) require golang.org/x/sys v0.12.0 // indirect golang-github-aead-minisign-0.3.0/go.sum000066400000000000000000000007231464252142200201030ustar00rootroot00000000000000golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang-github-aead-minisign-0.3.0/internal/000077500000000000000000000000001464252142200205625ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/internal/testdata/000077500000000000000000000000001464252142200223735ustar00rootroot00000000000000golang-github-aead-minisign-0.3.0/internal/testdata/message.txt000066400000000000000000000000151464252142200245540ustar00rootroot00000000000000Hello World! golang-github-aead-minisign-0.3.0/internal/testdata/message.txt.minisig000066400000000000000000000004531464252142200262200ustar00rootroot00000000000000untrusted comment: signature from minisign secret key RWRQhGcHOBlzwxrJCyuC+rJfHSfyRKRxkuwa3JJ0bWEs7RHjL1OUmqnTr+V1B9JzFuJIH/ybR2Eus9oEZKt9RbitpF/L4D3+5wg= trusted comment: timestamp:1614549543 file:message.txt P/722+ynQ+tIy0qadFHwLx5MsyNz/jDKJkDWQj4dDD2OKnVte8m/M14mwPE/1NMwzShPMSBhMXqZGdbe+UZjDg== golang-github-aead-minisign-0.3.0/internal/testdata/minisign-nopassword-0.key000066400000000000000000000004061464252142200272540ustar00rootroot00000000000000untrusted comment: minisign encrypted secret key RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= golang-github-aead-minisign-0.3.0/internal/testdata/minisign-nopassword-1.key000066400000000000000000000004111464252142200272510ustar00rootroot00000000000000untrusted comment: minisign encrypted secret key RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb/yydu4x5dcvbgaLZRtY5v8wFvgzMkvKyALUXUWcT+bvaqFvuvkUyUfMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= golang-github-aead-minisign-0.3.0/internal/testdata/minisign.key000066400000000000000000000004061464252142200247220ustar00rootroot00000000000000untrusted comment: minisign encrypted secret key RWRTY0Iytaz5znJmUO5kBt5xVkvpBl+29A7pZH86phD4h8vD3V8AAAACAAAAAAAAAEAAAAAA9vH9EcS6NdXNIEGhYGoqG1CiL4aptyJreJ4IfuT4+1h+OgVaY/vi0HsbCP0Y6n/wcy0AN0wOXmVDPP33jZqv82YCj2fH+/6MRuAfzNQYoLvc3sH/8bIwqdfpKIjDRZhvqRf063RFYoI= golang-github-aead-minisign-0.3.0/internal/testdata/minisign.pub000066400000000000000000000001611464252142200247160ustar00rootroot00000000000000untrusted comment: minisign public key C373193807678450 RWRQhGcHOBlzw4CoKyugkk4ioDfoxlXxC9LBx+VNhJ3w9w+cAxgvPsuo golang-github-aead-minisign-0.3.0/internal/testdata/robtest.ps1.minisig000066400000000000000000000004571464252142200261460ustar00rootroot00000000000000untrusted comment: signature from minisign secret key RWQ3ly9IPenQ6XE4gvV0tpJPSRdw/Si+Q4r97LbpLj0Hb3sV+XFydynJg3iFT2PjIlE3xViNOmFT9XrIoidedDr41+Ly0AYbUQg= trusted comment: timestamp:1617721023 file:robtest.ps1 HkxuqHSvipJo/unNKgDS+JGDB0+Q5d8nOeoJ0NGOnKBNsNdvAj8FWf7fhaPV7mzRJ1ooLvYpI0yUsD7lpaDwBQ== golang-github-aead-minisign-0.3.0/minisign.go000066400000000000000000000154741464252142200211250ustar00rootroot00000000000000// 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 { 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] } golang-github-aead-minisign-0.3.0/minisign.pub000066400000000000000000000001611464252142200212710ustar00rootroot00000000000000untrusted comment: minisign public key D7E531EE76B2FC6F RWRv/LJ27jHl10fMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0 golang-github-aead-minisign-0.3.0/minisign_test.go000066400000000000000000000033131464252142200221510ustar00rootroot00000000000000// 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 import ( "io" "os" "testing" ) func TestRoundtrip(t *testing.T) { const Password = "correct horse battery staple" privateKey, err := PrivateKeyFromFile(Password, "./internal/testdata/minisign.key") if err != nil { t.Fatalf("Failed to load private key: %v", err) } message, err := os.ReadFile("./internal/testdata/message.txt") if err != nil { t.Fatalf("Failed to load message: %v", err) } signature := Sign(privateKey, message) publicKey, err := PublicKeyFromFile("./internal/testdata/minisign.pub") if err != nil { t.Fatalf("Failed to load public key: %v", err) } if !Verify(publicKey, message, signature) { t.Fatalf("Verification failed: signature %q - public key %q", signature, publicKey) } } func TestReaderRoundtrip(t *testing.T) { const Password = "correct horse battery staple" privateKey, err := PrivateKeyFromFile(Password, "./internal/testdata/minisign.key") if err != nil { t.Fatalf("Failed to load private key: %v", err) } file, err := os.Open("./internal/testdata/message.txt") if err != nil { t.Fatalf("Failed to open message: %v", err) } defer file.Close() reader := NewReader(file) if _, err = io.Copy(io.Discard, reader); err != nil { t.Fatalf("Failed to read message: %v", err) } signature := reader.Sign(privateKey) publicKey, err := PublicKeyFromFile("./internal/testdata/minisign.pub") if err != nil { t.Fatalf("Failed to load public key: %v", err) } if !reader.Verify(publicKey, signature) { t.Fatalf("Verification failed: signature %q - public key %q", signature, publicKey) } } golang-github-aead-minisign-0.3.0/private.go000066400000000000000000000273301464252142200207540ustar00rootroot00000000000000// 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 import ( "crypto" "crypto/ed25519" "crypto/rand" "crypto/subtle" "encoding/base64" "encoding/binary" "errors" "fmt" "io" "os" "strconv" "strings" "time" "golang.org/x/crypto/blake2b" "golang.org/x/crypto/scrypt" ) // PrivateKeyFromFile reads and decrypts the private key // file with the given password. func PrivateKeyFromFile(password, path string) (PrivateKey, error) { bytes, err := os.ReadFile(path) if err != nil { return PrivateKey{}, err } return DecryptKey(password, bytes) } // PrivateKey is a minisign private key. // // A private key can sign messages to prove their origin and authenticity. // // PrivateKey implements the crypto.Signer interface. type PrivateKey struct { _ [0]func() // prevent direct comparison: p1 == p2. id uint64 bytes [ed25519.PrivateKeySize]byte } var _ crypto.Signer = (*PrivateKey)(nil) // compiler check // ID returns the 64 bit key ID. func (p PrivateKey) ID() uint64 { return p.id } // Public returns the corresponding public key. func (p PrivateKey) Public() crypto.PublicKey { var bytes [ed25519.PublicKeySize]byte copy(bytes[:], p.bytes[32:]) return PublicKey{ id: p.ID(), bytes: bytes, } } // Sign signs the given message. // // The minisign signature scheme relies on Ed25519 and supports // plain as well as pre-hashed messages. Therefore, opts can be // either crypto.Hash(0) to signal that the message has not been // hashed or crypto.BLAKE2b_512 to signal that the message is a // BLAKE2b-512 digest. If opts is crypto.BLAKE2b_512 then message // must be a 64 bytes long. // // Minisign signatures are deterministic such that no randomness // is necessary. func (p PrivateKey) Sign(_ io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { var ( trustedComment = "timestamp:" + strconv.FormatInt(time.Now().Unix(), 10) untrustedComment = "signature from private key: " + strings.ToUpper(strconv.FormatUint(p.ID(), 16)) ) switch h := opts.HashFunc(); h { case crypto.Hash(0): const isHashed = false return sign(p, message, trustedComment, untrustedComment, isHashed), nil case crypto.BLAKE2b_512: const isHashed = true if n := len(message); n != blake2b.Size { return nil, errors.New("minisign: invalid message length " + strconv.Itoa(n)) } return sign(p, message, trustedComment, untrustedComment, isHashed), nil default: return nil, errors.New("minisign: cannot sign messages hashed with " + strconv.Itoa(int(h))) } } // Equal returns true if and only if p and x have equivalent values. func (p PrivateKey) Equal(x crypto.PrivateKey) bool { xx, ok := x.(PrivateKey) if !ok { return false } 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) { // 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) binary.LittleEndian.PutUint16(b[2:], algorithmNone) binary.LittleEndian.PutUint16(b[4:], algorithmBlake2b) 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)) 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 ( 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) } 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) } p.id = binary.LittleEndian.Uint64(key) copy(p.bytes[:], key[8:]) return nil } const ( 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 privateKeySize = 158 // 2 + 2 + 2 + 32 + 8 + 8 + 104 ) // EncryptKey encrypts the private key with the given password // using some entropy from the RNG of the OS. func EncryptKey(password string, privateKey PrivateKey) ([]byte, error) { var privateKeyBytes [72]byte binary.LittleEndian.PutUint64(privateKeyBytes[:], privateKey.ID()) copy(privateKeyBytes[8:], privateKey.bytes[:]) var salt [32]byte if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { return nil, err } var bytes [privateKeySize]byte binary.LittleEndian.PutUint16(bytes[0:], EdDSA) 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 defaultMem = 1073741824 // libsodium MEM_LIMIT_SENSITIVE ) copy(bytes[6:38], salt[:]) binary.LittleEndian.PutUint64(bytes[38:], defaultOps) binary.LittleEndian.PutUint64(bytes[46:], defaultMem) copy(bytes[54:], encryptKey(password, salt[:], defaultOps, defaultMem, privateKeyBytes[:])) const comment = "untrusted comment: minisign encrypted secret key\n" encodedBytes := make([]byte, len(comment)+base64.StdEncoding.EncodedLen(len(bytes))) copy(encodedBytes, []byte(comment)) base64.StdEncoding.Encode(encodedBytes[len(comment):], bytes[:]) 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] return len(bytes) >= 4 && binary.LittleEndian.Uint16(bytes[2:]) == algorithmScrypt } var errDecrypt = errors.New("minisign: decryption failed") // DecryptKey tries to decrypt the encrypted private key with // the given password. func DecryptKey(password string, privateKey []byte) (PrivateKey, error) { privateKey = trimUntrustedComment(privateKey) b := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey))) n, err := base64.StdEncoding.Decode(b, privateKey) if err != nil { return PrivateKey{}, err } b = b[:n] if len(b) != privateKeySize { return PrivateKey{}, errDecrypt } 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 kdf != algorithmScrypt { return PrivateKey{}, errDecrypt } if hType != algorithmBlake2b { return PrivateKey{}, errDecrypt } if scryptOps > scryptOpsLimit { return PrivateKey{}, errDecrypt } if scryptMem > scryptMemLimit { return PrivateKey{}, errDecrypt } plaintext, err := decryptKey(password, salt, scryptOps, scryptMem, ciphertext) if err != nil { return PrivateKey{}, err } key := PrivateKey{ id: binary.LittleEndian.Uint64(plaintext), } copy(key.bytes[:], plaintext[8:]) return key, nil } // encryptKey encrypts the plaintext and returns a ciphertext by: // 1. tag = BLAKE2b-256(EdDSA-const || plaintext) // 2. keystream = Scrypt(password, salt, convert(ops, mem)) // 3. ciphertext = (plaintext || tag) ⊕ keystream // // Therefore, decryptKey converts the ops and mem cost parameters // to the (N, r, p)-tuple expected by Scrypt. // // The plaintext must be a private key ID concatenated with a raw // Ed25519 private key, and therefore, 72 bytes long. func encryptKey(password string, salt []byte, ops, mem uint64, plaintext []byte) []byte { const ( plaintextLen = 72 messageLen = 74 ciphertextLen = 104 ) N, r, p := convertScryptParameters(ops, mem) keystream, err := scrypt.Key([]byte(password), salt, N, r, p, ciphertextLen) if err != nil { panic(err) } var message [messageLen]byte binary.LittleEndian.PutUint16(message[:2], EdDSA) copy(message[2:], plaintext) checksum := blake2b.Sum256(message[:]) var ciphertext [ciphertextLen]byte copy(ciphertext[:plaintextLen], plaintext) copy(ciphertext[plaintextLen:], checksum[:]) for i, k := range keystream { ciphertext[i] ^= k } return ciphertext[:] } // decryptKey decrypts the ciphertext and returns a plaintext by: // 1. keystream = Scrypt(password, salt, convert(ops, mem)) // 2. plaintext || tag = ciphertext ⊕ keystream // 3. Check that: tag == BLAKE2b-256(EdDSA-const || plaintext) // // Therefore, decryptKey converts the ops and mem cost parameters to // the (N, r, p)-tuple expected by Scrypt. // // It returns an error if the ciphertext is not valid - i.e. if the // tag does not match the BLAKE2b-256 hash value. func decryptKey(password string, salt []byte, ops, mem uint64, ciphertext []byte) ([]byte, error) { const ( plaintextLen = 72 messageLen = 74 ciphertextLen = 104 ) if len(ciphertext) != ciphertextLen { return nil, errDecrypt } N, r, p := convertScryptParameters(ops, mem) keystream, err := scrypt.Key([]byte(password), salt, N, r, p, ciphertextLen) if err != nil { return nil, err } var plaintext [ciphertextLen]byte for i, k := range keystream { plaintext[i] = ciphertext[i] ^ k } var ( privateKeyBytes = plaintext[:plaintextLen] checksum = plaintext[plaintextLen:] ) var message [messageLen]byte binary.LittleEndian.PutUint16(message[:2], EdDSA) copy(message[2:], privateKeyBytes) if sum := blake2b.Sum256(message[:]); subtle.ConstantTimeCompare(sum[:], checksum) != 1 { return nil, errDecrypt } return privateKeyBytes, nil } // convertScryptParameters converts the operational and memory cost // to the Scrypt parameters N, r and p. // // N is the overall memory / CPU cost and r * p has to be lower then // 2³⁰. Refer to the scrypt.Key docs for more information. func convertScryptParameters(ops, mem uint64) (N, r, p int) { const ( minOps = 1 << 15 maxRP = 0x3fffffff ) if ops < minOps { ops = minOps } if ops < mem/32 { r, p = 8, 1 for n := 1; n < 63; n++ { if N = 1 << n; uint64(N) > (ops / (8 * uint64(r))) { break } } } else { r = 8 for n := 1; n < 63; n++ { if N = 1 << n; uint64(N) > (mem / (256 * uint64(r))) { break } } if rp := (ops / 4) / uint64(N); rp < maxRP { p = int(rp) / r } else { p = maxRP / r } } return N, r, p } golang-github-aead-minisign-0.3.0/private_test.go000066400000000000000000000053261464252142200220140ustar00rootroot00000000000000// 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" "encoding/base64" "os" "testing" ) var marshalPrivateKeyTests = []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_Marshal(t *testing.T) { 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, } 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, raw) { t.Fatalf("Test %d: failed to marshal private key:\nGot: %v\nWant: %v\n", i, text, raw) } } } 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, test := range unmarshalPrivateKeyTests { raw, err := os.ReadFile(test.File) if err != nil { t.Fatalf("Test %d: failed to read private key: %v", i, err) } var key PrivateKey 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: // t.Logf("\n{\n\tID: htoi(\"%X\"),\n\tBytes: b64(\"%s\"),\n}", key.id, base64.StdEncoding.EncodeToString(key.bytes[:])) 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) } } } func b64(s string) []byte { b, err := base64.StdEncoding.DecodeString(s) if err != nil { panic(err) } return b } golang-github-aead-minisign-0.3.0/public.go000066400000000000000000000055141464252142200205600ustar00rootroot00000000000000// 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 import ( "crypto" "crypto/ed25519" "encoding/base64" "encoding/binary" "errors" "fmt" "os" "strconv" "strings" ) 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 } var key PublicKey if err = key.UnmarshalText(bytes); err != nil { return PublicKey{}, err } return key, nil } // PublicKey is a minisign public key. // // A public key is used to verify whether messages // have been signed with the corresponding private // key. type PublicKey struct { _ [0]func() // prevent direct comparison: p1 == p2. id uint64 bytes [ed25519.PublicKeySize]byte } // ID returns the 64 bit key ID. func (p PublicKey) ID() uint64 { return p.id } // Equal returns true if and only if p and x have equivalent values. func (p PublicKey) Equal(x crypto.PublicKey) bool { xx, ok := x.(PublicKey) if !ok { return false } return p.id == xx.id && p.bytes == xx.bytes } // String returns a base64 string representation of the PublicKey p. func (p PublicKey) String() string { var bytes [publicKeySize]byte binary.LittleEndian.PutUint16(bytes[:2], EdDSA) binary.LittleEndian.PutUint64(bytes[2:10], p.ID()) copy(bytes[10:], p.bytes[:]) return base64.StdEncoding.EncodeToString(bytes[:]) } // MarshalText returns a textual representation of the PublicKey p. // // It never returns an error. func (p PublicKey) MarshalText() ([]byte, error) { 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 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))) n, err := base64.StdEncoding.Decode(bytes, text) if err != nil { return fmt.Errorf("minisign: invalid public key: %v", err) } bytes = bytes[:n] // Adjust since text may contain '\r' or '\n' which would have been ignored during decoding. 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 { return errors.New("minisign: invalid public key algorithm " + strconv.Itoa(int(a))) } p.id = binary.LittleEndian.Uint64(bytes[2:10]) copy(p.bytes[:], bytes[10:]) return nil } golang-github-aead-minisign-0.3.0/public_test.go000066400000000000000000000060371464252142200216200ustar00rootroot00000000000000// 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 import "testing" var marshalPublicKeyTests = []struct { PublicKey PublicKey Text string }{ { PublicKey: PublicKey{ id: 0xe7620f1842b4e81f, bytes: [32]byte{121, 165, 97, 231, 14, 224, 140, 211, 231, 84, 198, 62, 155, 214, 185, 195, 82, 10, 29, 66, 4, 205, 16, 77, 162, 231, 239, 118, 59, 24, 83, 183}, }, Text: "untrusted comment: minisign public key: E7620F1842B4E81F" + "\n" + "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3", }, { PublicKey: PublicKey{ id: 0x6f7add142cdc7edb, bytes: [32]byte{121, 165, 97, 231, 14, 224, 140, 211, 231, 84, 198, 62, 155, 214, 185, 195, 82, 10, 29, 66, 4, 205, 16, 77, 162, 231, 239, 118, 59, 24, 83, 183}, }, Text: "untrusted comment: minisign public key: 6F7ADD142CDC7EDB" + "\n" + "RWTbftwsFN16b3mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3", }, } func TestMarshalPublicKey(t *testing.T) { for i, test := range marshalPublicKeyTests { text, err := test.PublicKey.MarshalText() if err != nil { t.Fatalf("Test %d: failed to marshal public key: %v", i, err) } if string(text) != test.Text { t.Fatalf("Test %d: got '%s' - want '%s'", i, string(text), test.Text) } } } var unmarshalPublicKeyTests = []struct { Text string PublicKey PublicKey ShouldFail bool }{ { Text: "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3", PublicKey: PublicKey{ id: 0xe7620f1842b4e81f, bytes: [32]byte{121, 165, 97, 231, 14, 224, 140, 211, 231, 84, 198, 62, 155, 214, 185, 195, 82, 10, 29, 66, 4, 205, 16, 77, 162, 231, 239, 118, 59, 24, 83, 183}, }, }, { Text: "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3\r\n\n", PublicKey: PublicKey{ id: 0xe7620f1842b4e81f, bytes: [32]byte{121, 165, 97, 231, 14, 224, 140, 211, 231, 84, 198, 62, 155, 214, 185, 195, 82, 10, 29, 66, 4, 205, 16, 77, 162, 231, 239, 118, 59, 24, 83, 183}, }, }, { // Invalid algorithm Text: "RmQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3", ShouldFail: true, }, { // Invalid public key b/c too long Text: "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3bhQ=", ShouldFail: true, }, } func TestUnmarshalPublicKey(t *testing.T) { for i, test := range unmarshalPublicKeyTests { var key PublicKey err := key.UnmarshalText([]byte(test.Text)) if err == nil && test.ShouldFail { t.Fatalf("Test %d: should have failed but passed", i) } if err != nil && !test.ShouldFail { t.Fatalf("Test %d: failed to unmarshal public key: %v", i, err) } if err == nil { if key.ID() != test.PublicKey.ID() { t.Fatalf("Test %d: key ID mismatch: got '%x' - want '%x'", i, key.ID(), test.PublicKey.ID()) } if key.bytes != test.PublicKey.bytes { t.Fatalf("Test %d: raw public key mismatch: got '%v' - want '%v'", i, key.bytes, test.PublicKey.bytes) } if !key.Equal(test.PublicKey) { t.Fatalf("Test %d: public keys are not equal", i) } } } } golang-github-aead-minisign-0.3.0/signature.go000066400000000000000000000145421464252142200213040ustar00rootroot00000000000000// 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 import ( "crypto/ed25519" "encoding/base64" "encoding/binary" "errors" "fmt" "os" "strconv" "strings" ) // 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 } var signature Signature if err = signature.UnmarshalText(bytes); err != nil { return Signature{}, err } return signature, nil } // Signature is a structured representation of a minisign // signature. // // A signature is generated when signing a message with // a private key: // // signature = Sign(privateKey, message) // // The signature of a message can then be verified with the // corresponding public key: // // if Verify(publicKey, message, signature) { // // => signature is valid // // => message has been signed with correspoding private key // } type Signature struct { _ [0]func() // enforce named assignment and prevent direct comparison // 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 // to produce this signature. It can be used to identify the // corresponding public key that can verify the signature. // // However, key IDs are random identifiers and not protected at all. // A key ID is just a hint to quickly identify a public key candidate. KeyID uint64 // TrustedComment is a comment that has been signed and is // verified during signature verification. TrustedComment string // UntrustedComment is a comment that has not been signed // and is not verified during signature verification. // // It must not be considered authentic - in contrast to the // TrustedComment. UntrustedComment string // Signature is the Ed25519 signature of the message that // has been signed. Signature [ed25519.SignatureSize]byte // CommentSignature is the Ed25519 signature of Signature // concatenated with the TrustedComment: // // CommentSignature = ed25519.Sign(PrivateKey, Signature || TrustedComment) // // It is used to verify that the TrustedComment is authentic. CommentSignature [ed25519.SignatureSize]byte } // String returns a string representation of the Signature s. // // In contrast to MarshalText, String does not fail if s is // not a valid minisign signature. func (s Signature) String() string { return string(encodeSignature(&s)) } // Equal reports whether s and x have equivalent values. // // The untrusted comments of two equivalent signatures may differ. func (s Signature) Equal(x Signature) bool { return s.Algorithm == x.Algorithm && s.KeyID == x.KeyID && s.Signature == x.Signature && s.CommentSignature == x.CommentSignature && s.TrustedComment == x.TrustedComment } // MarshalText returns a textual representation of the Signature s. // // 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 encodeSignature(&s), nil } // 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 { return errors.New("minisign: invalid signature") } var ( untrustedComment = strings.TrimRight(segments[0], "\r") encodedSignature = segments[1] trustedComment = strings.TrimRight(segments[2], "\r") encodedCommentSignature = segments[3] ) if !strings.HasPrefix(untrustedComment, "untrusted comment: ") { return errors.New("minisign: invalid signature: invalid untrusted comment") } if !strings.HasPrefix(trustedComment, "trusted comment: ") { return errors.New("minisign: invalid signature: invalid trusted comment") } rawSignature, err := base64.StdEncoding.DecodeString(encodedSignature) if err != nil { return fmt.Errorf("minisign: invalid signature: %v", err) } if n := len(rawSignature); n != 2+8+ed25519.SignatureSize { return errors.New("minisign: invalid signature length " + strconv.Itoa(n)) } commentSignature, err := base64.StdEncoding.DecodeString(encodedCommentSignature) if err != nil { return fmt.Errorf("minisign: invalid signature: %v", err) } if n := len(commentSignature); n != ed25519.SignatureSize { return errors.New("minisign: invalid comment signature length " + strconv.Itoa(n)) } var ( algorithm = binary.LittleEndian.Uint16(rawSignature[:2]) keyID = binary.LittleEndian.Uint64(rawSignature[2:10]) ) if algorithm != EdDSA && algorithm != HashEdDSA { return errors.New("minisign: invalid signature: invalid algorithm " + strconv.Itoa(int(algorithm))) } s.Algorithm = algorithm s.KeyID = keyID s.TrustedComment = strings.TrimPrefix(trustedComment, "trusted comment: ") s.UntrustedComment = strings.TrimPrefix(untrustedComment, "untrusted comment: ") copy(s.Signature[:], rawSignature[10:]) 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') } golang-github-aead-minisign-0.3.0/signature_test.go000066400000000000000000000342651464252142200223470ustar00rootroot00000000000000// 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 import ( "strings" "testing" ) func TestEqualSignature(t *testing.T) { for i, test := range equalSignatureTests { equal := test.A.Equal(test.B) if equal != test.Equal { t.Fatalf("Test %d: got 'equal=%v' - want 'equal=%v", i, equal, test.Equal) } if revEqual := test.B.Equal(test.A); equal != revEqual { t.Fatalf("Test %d: A == B is %v but B == A is %v", i, equal, revEqual) } } } func TestMarshalInvalidSignature(t *testing.T) { var signature Signature if _, err := signature.MarshalText(); err == nil { t.Fatal("Marshaling invalid signature succeeded") } } func TestMarshalSignatureRoundtrip(t *testing.T) { for i, test := range marshalSignatureTests { text, err := test.Signature.MarshalText() if err != nil { t.Fatalf("Test %d: failed to marshal signature: %v", i, err) } var signature Signature if err = signature.UnmarshalText(text); err != nil { t.Fatalf("Test %d: failed to unmarshal signature: %v", i, err) } if !signature.Equal(test.Signature) { t.Fatalf("Test %d: signature mismatch: got '%v' - want '%v'", i, signature, test.Signature) } } } func TestUnmarshalSignature(t *testing.T) { for i, test := range unmarshalSignatureTests { var signature Signature err := signature.UnmarshalText([]byte(test.Text)) if err == nil && test.ShouldFail { t.Fatalf("Test %d: should have failed but passed", i) } if err != nil && !test.ShouldFail { t.Fatalf("Test %d: failed to unmarshal signature: %v", i, err) } if err == nil { if !signature.Equal(test.Signature) { t.Fatalf("Test %d: signatures are not equal: got '%s' - want '%s'", i, signature, test.Signature) } if signature.UntrustedComment != test.Signature.UntrustedComment { t.Fatalf("Test %d: untrusted comment mismatch: got '%s' - want '%s'", i, signature.UntrustedComment, test.Signature.UntrustedComment) } } } } func TestSignatureCarriageReturn(t *testing.T) { signature, err := SignatureFromFile("./internal/testdata/robtest.ps1.minisig") if err != nil { t.Fatalf("Failed to read signature from file: %v", err) } if strings.HasSuffix(signature.UntrustedComment, "\r") { t.Fatal("Untrusted comment ends with a carriage return") } if strings.HasSuffix(signature.TrustedComment, "\r") { t.Fatal("Trusted comment ends with a carriage return") } } var equalSignatureTests = []struct { A, B Signature Equal bool }{ { A: Signature{}, B: Signature{}, Equal: true, }, { A: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}, }, B: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}, }, Equal: true, }, { A: Signature{UntrustedComment: "signature A"}, B: Signature{UntrustedComment: "signature B"}, Equal: true, }, { A: Signature{Algorithm: EdDSA}, B: Signature{Algorithm: HashEdDSA}, Equal: false, // Algorithm differs }, { A: Signature{KeyID: 0xe7620f1842b4e81f}, B: Signature{KeyID: 0x1fe8b442180f62e7}, Equal: false, // KeyID differs }, { A: Signature{TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`}, B: Signature{TrustedComment: `timestamp:1591521249 file:minisign-0.9.tar.gz`}, Equal: false, // TrustedComment differs }, { A: Signature{Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}}, B: Signature{Signature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}}, Equal: false, // Signature differs }, { A: Signature{CommentSignature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}}, B: Signature{CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}}, Equal: false, // CommentSignature differs }, } var marshalSignatureTests = []struct { Signature Signature }{ { Signature: Signature{ Algorithm: EdDSA, }, }, { Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, }, }, { Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, }, }, { Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, }, }, { Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, }, }, { Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}, }, }, } var unmarshalSignatureTests = []struct { Text string Signature Signature ShouldFail bool }{ { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}, }, }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==` + "\n\r\n", Signature: Signature{ Algorithm: EdDSA, KeyID: 0xe7620f1842b4e81f, UntrustedComment: `signature from minisign secret key`, TrustedComment: `timestamp:1591521248 file:minisign-0.9.tar.gz`, Signature: [64]byte{20, 99, 118, 100, 132, 21, 202, 44, 47, 123, 240, 66, 228, 28, 175, 132, 143, 49, 11, 188, 252, 49, 53, 73, 106, 154, 66, 249, 67, 203, 35, 77, 156, 24, 226, 182, 244, 241, 252, 5, 244, 97, 127, 41, 191, 156, 128, 14, 117, 64, 157, 164, 36, 146, 238, 203, 151, 33, 174, 82, 239, 66, 73, 10}, CommentSignature: [64]byte{148, 178, 205, 92, 217, 151, 10, 78, 112, 147, 154, 17, 47, 24, 233, 136, 141, 16, 37, 217, 29, 77, 64, 75, 217, 55, 69, 178, 114, 188, 40, 93, 6, 130, 93, 121, 211, 7, 19, 198, 190, 160, 33, 49, 136, 129, 80, 249, 121, 170, 165, 216, 105, 97, 230, 151, 208, 109, 244, 227, 46, 121, 241, 15}, }, }, // Invalid signatures { Text: `RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, ShouldFail: true, // Missing untrusted comment }, { Text: `untrusted: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, ShouldFail: true, // Invalid untrusted comment - wrong prefix }, { Text: `untrusted comment: signature from minisign secret key trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, ShouldFail: true, // Missing signature value }, { Text: `untrusted comment: signature from minisign secret key 31TR+QBxE86BOJz1U46pc1lM1zEvMLBDTE255CHxFFLFcn4qPd3Q77xJTF2Y2IkDNqrTOCaZ43PQjSv9kIrnHXXwW0dwKnj trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, ShouldFail: true, // Invalid signature value - invalid base64 }, { Text: `untrusted comment: signature from minisign secret key f4IYNY3p6K5CYtfB+dhN6Y+Fi+F6wWI0r+VjLwDE0q23wB1Opso6w/MJd9YGIU/HBs04flXnak37x/s2QhWAZlSCdbQYX7Q= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==`, ShouldFail: true, // Invalid signature value - invalid size }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==` + "\n\r\n", ShouldFail: true, // Missing trusted comment }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= comment: timestamp:1591521248 file:minisign-0.9.tar.gz lLLNXNmXCk5wk5oRLxjpiI0QJdkdTUBL2TdFsnK8KF0Ggl150wcTxr6gITGIgVD5eaql2Glh5pfQbfTjLnnxDw==` + "\n\r\n", ShouldFail: true, // Invalid trusted comment - wrong prefix }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz`, ShouldFail: true, // Missing comment signature }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz Bqq219+sDloDkxHiCLcR5sTxrbl+qMS4oEnZ+IrZ4JDH5BxAzKehjoWSch3nbyNT96c/jz+XQjj4zd492skB_w==`, ShouldFail: true, // Invalid comment signature - invalid base64 }, { Text: `untrusted comment: signature from minisign secret key RWQf6LRCGA9i5xRjdmSEFcosL3vwQuQcr4SPMQu8/DE1SWqaQvlDyyNNnBjitvTx/AX0YX8pv5yADnVAnaQkku7LlyGuUu9CSQo= trusted comment: timestamp:1591521248 file:minisign-0.9.tar.gz nqGtUS55Xhx/VzvCGtWjtsnlcItcsp0hzl/40j3oRkyJAISXHTakVQKK2VBBMyjBfhZTRRlEputvn/dNdC/Dh6Y=`, ShouldFail: true, // Invalid comment signature - invalid size }, }