pax_global_header00006660000000000000000000000064152056244070014517gustar00rootroot0000000000000052 comment=a09352b57a22f418b9ad8edcfe901eb4be8d40e5 jedisct1-go-minisign-4975600/000077500000000000000000000000001520562440700156435ustar00rootroot00000000000000jedisct1-go-minisign-4975600/.github/000077500000000000000000000000001520562440700172035ustar00rootroot00000000000000jedisct1-go-minisign-4975600/.github/dependabot.yml000066400000000000000000000002211520562440700220260ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 jedisct1-go-minisign-4975600/.github/workflows/000077500000000000000000000000001520562440700212405ustar00rootroot00000000000000jedisct1-go-minisign-4975600/.github/workflows/go.yml000066400000000000000000000005461520562440700223750ustar00rootroot00000000000000name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1 - name: Build run: go build -v ./... - name: Test run: go test -v ./... jedisct1-go-minisign-4975600/.gitignore000066400000000000000000000004231520562440700176320ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ jedisct1-go-minisign-4975600/LICENSE000066400000000000000000000020611520562440700166470ustar00rootroot00000000000000MIT License Copyright (c) 2018-2024 Frank Denis 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. jedisct1-go-minisign-4975600/README.md000066400000000000000000000001501520562440700171160ustar00rootroot00000000000000# go-minisign A Golang library to verify [Minisign](https://jedisct1.github.io/minisign/) signatures. jedisct1-go-minisign-4975600/go.mod000066400000000000000000000002051520562440700167460ustar00rootroot00000000000000module github.com/jedisct1/go-minisign go 1.25.0 require golang.org/x/crypto v0.52.0 require golang.org/x/sys v0.45.0 // indirect jedisct1-go-minisign-4975600/go.sum000066400000000000000000000004701520562440700167770ustar00rootroot00000000000000golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= jedisct1-go-minisign-4975600/minisign.go000066400000000000000000000074041520562440700200140ustar00rootroot00000000000000package minisign import ( "crypto/ed25519" "encoding/base64" "errors" "os" "strings" "golang.org/x/crypto/blake2b" ) type PublicKey struct { SignatureAlgorithm [2]byte KeyId [8]byte PublicKey [32]byte } type Signature struct { UntrustedComment string SignatureAlgorithm [2]byte KeyId [8]byte Signature [64]byte TrustedComment string GlobalSignature [64]byte } func NewPublicKey(publicKeyStr string) (PublicKey, error) { var publicKey PublicKey bin, err := base64.StdEncoding.DecodeString(publicKeyStr) if err != nil || len(bin) != 42 { return publicKey, errors.New("Invalid encoded public key") } copy(publicKey.SignatureAlgorithm[:], bin[0:2]) copy(publicKey.KeyId[:], bin[2:10]) copy(publicKey.PublicKey[:], bin[10:42]) return publicKey, nil } func DecodePublicKey(in string) (PublicKey, error) { var publicKey PublicKey lines := strings.SplitN(in, "\n", 2) if len(lines) < 2 { return publicKey, errors.New("Incomplete encoded public key") } return NewPublicKey(lines[1]) } func trimCarriageReturn(input string) string { return strings.TrimRight(input, "\r") } func DecodeSignature(in string) (Signature, error) { var signature Signature lines := strings.SplitN(in, "\n", 4) if len(lines) < 4 { return signature, errors.New("Incomplete encoded signature") } signature.UntrustedComment = trimCarriageReturn(lines[0]) bin1, err := base64.StdEncoding.DecodeString(lines[1]) if err != nil || len(bin1) != 74 { return signature, errors.New("Invalid encoded signature") } signature.TrustedComment = trimCarriageReturn(lines[2]) bin2, err := base64.StdEncoding.DecodeString(lines[3]) if err != nil || len(bin2) != 64 { return signature, errors.New("Invalid encoded signature") } copy(signature.SignatureAlgorithm[:], bin1[0:2]) copy(signature.KeyId[:], bin1[2:10]) copy(signature.Signature[:], bin1[10:74]) copy(signature.GlobalSignature[:], bin2) return signature, nil } func NewPublicKeyFromFile(file string) (PublicKey, error) { var publicKey PublicKey bin, err := os.ReadFile(file) if err != nil { return publicKey, err } return DecodePublicKey(string(bin)) } func NewSignatureFromFile(file string) (Signature, error) { var signature Signature bin, err := os.ReadFile(file) if err != nil { return signature, err } return DecodeSignature(string(bin)) } func (publicKey *PublicKey) Verify(bin []byte, signature Signature) (bool, error) { if publicKey.SignatureAlgorithm != [2]byte{'E', 'd'} { return false, errors.New("Incompatible signature algorithm") } prehashed := false if signature.SignatureAlgorithm[0] == 0x45 && signature.SignatureAlgorithm[1] == 0x64 { prehashed = false } else if signature.SignatureAlgorithm[0] == 0x45 && signature.SignatureAlgorithm[1] == 0x44 { prehashed = true } else { return false, errors.New("Unsupported signature algorithm") } if publicKey.KeyId != signature.KeyId { return false, errors.New("Incompatible key identifiers") } if !strings.HasPrefix(signature.TrustedComment, "trusted comment: ") { return false, errors.New("Unexpected format for the trusted comment") } if prehashed { h, _ := blake2b.New512(nil) h.Write(bin) bin = h.Sum(nil) } if !ed25519.Verify(ed25519.PublicKey(publicKey.PublicKey[:]), bin, signature.Signature[:]) { return false, errors.New("Invalid signature") } if !ed25519.Verify(ed25519.PublicKey(publicKey.PublicKey[:]), append(signature.Signature[:], []byte(signature.TrustedComment)[17:]...), signature.GlobalSignature[:]) { return false, errors.New("Invalid global signature") } return true, nil } func (publicKey *PublicKey) VerifyFromFile(file string, signature Signature) (bool, error) { bin, err := os.ReadFile(file) if err != nil { return false, err } return publicKey.Verify(bin, signature) } jedisct1-go-minisign-4975600/minisign_test.go000066400000000000000000000025521520562440700210520ustar00rootroot00000000000000package minisign import "testing" func TestLegacy(t *testing.T) { pk, err := NewPublicKey("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3") if err != nil { t.Fatal(err) } sigStr := "untrusted comment: signature from minisign secret key\nRWQf6LRCGA9i59SLOFxz6NxvASXDJeRtuZykwQepbDEGt87ig1BNpWaVWuNrm73YiIiJbq71Wi+dP9eKL8OC351vwIasSSbXxwA=\ntrusted comment: timestamp:1635442742\tfile:test\n0YteLgV960ia80vnA/fHbvkyjl/IoP/HNOCaZfrF0CdhAlp7ok+Tpkya+VpWPX5C/Is3q8a/kEDSY7fBmmgJCg==\n" sig, err := DecodeSignature(sigStr) if err != nil { t.Fatal(err) } v, err := pk.Verify([]byte("test"), sig) if err != nil { t.Fatal(err) } if !v { t.Fatal("signature verification failed") } } func TestPrehashed(t *testing.T) { pk, err := NewPublicKey("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3") if err != nil { t.Fatal(err) } sigStr := "untrusted comment: signature from minisign secret key\nRUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=\ntrusted comment: timestamp:1635443258\tfile:test\thashed\n/cj37GK60vryibFn+ftOgbCvW9NKhKYgjVpFFQUcWPAnjO23wrvVDTt7cloNC06maoBli9q6qwZDXXoaxweICQ==\n" sig, err := DecodeSignature(sigStr) if err != nil { t.Fatal(err) } v, err := pk.Verify([]byte("test"), sig) if err != nil { t.Fatal(err) } if !v { t.Fatal("signature verification failed") } } jedisct1-go-minisign-4975600/sign.go000066400000000000000000000251331520562440700171360ustar00rootroot00000000000000package minisign import ( "bytes" "crypto/ed25519" "crypto/subtle" "encoding/base64" "encoding/binary" "errors" "fmt" "io" "os" "path/filepath" "strings" "time" "golang.org/x/crypto/blake2b" "golang.org/x/crypto/scrypt" ) const ( sigAlgEd = "Ed" kdfAlgScrypt = "Sc" chkAlgBlake2b = "B2" commentPrefix = "untrusted comment: " trustedPrefix = "trusted comment: " defaultSigUntrusted = "signature from minisign secret key" secretKeyLen = 158 streamLen = 104 // trustedCommentMaxLen mirrors TRUSTEDCOMMENTMAXBYTES in the C // reference: any longer line cannot be read back by minisign(1). trustedCommentMaxLen = 8192 - len(trustedPrefix) ) // PrivateKey is a minisign Ed25519 secret key. It may be stored in encrypted // form; call Decrypt with the passphrase before signing in that case. type PrivateKey struct { UntrustedComment string SignatureAlgorithm [2]byte KDFAlgorithm [2]byte ChecksumAlgorithm [2]byte KDFSalt [32]byte KDFOpsLimit uint64 KDFMemLimit uint64 KeyId [8]byte SecretKey [64]byte Checksum [32]byte } // IsEncrypted reports whether the key material is still encrypted under a // passphrase. Calling Sign on an encrypted key returns an error. func (sk *PrivateKey) IsEncrypted() bool { return sk.KDFAlgorithm != [2]byte{0, 0} } // NewPrivateKey parses a single base64-encoded private key payload. func NewPrivateKey(in string) (PrivateKey, error) { var sk PrivateKey bin, err := base64.StdEncoding.DecodeString(trimCarriageReturn(in)) if err != nil || len(bin) != secretKeyLen { return sk, errors.New("Invalid encoded secret key") } copy(sk.SignatureAlgorithm[:], bin[0:2]) copy(sk.KDFAlgorithm[:], bin[2:4]) copy(sk.ChecksumAlgorithm[:], bin[4:6]) copy(sk.KDFSalt[:], bin[6:38]) sk.KDFOpsLimit = binary.LittleEndian.Uint64(bin[38:46]) sk.KDFMemLimit = binary.LittleEndian.Uint64(bin[46:54]) copy(sk.KeyId[:], bin[54:62]) copy(sk.SecretKey[:], bin[62:126]) copy(sk.Checksum[:], bin[126:158]) if string(sk.SignatureAlgorithm[:]) != sigAlgEd { return sk, errors.New("Unsupported signature algorithm") } if string(sk.ChecksumAlgorithm[:]) != chkAlgBlake2b { return sk, errors.New("Unsupported checksum algorithm") } return sk, nil } // DecodePrivateKey parses a full minisign secret key file (comment line + // base64 payload). func DecodePrivateKey(in string) (PrivateKey, error) { lines := strings.SplitN(in, "\n", 2) if len(lines) < 2 { return PrivateKey{}, errors.New("Incomplete encoded secret key") } sk, err := NewPrivateKey(lines[1]) if err != nil { return sk, err } sk.UntrustedComment = trimCarriageReturn(lines[0]) return sk, nil } // NewPrivateKeyFromFile reads and parses a minisign secret key file. func NewPrivateKeyFromFile(file string) (PrivateKey, error) { bin, err := os.ReadFile(file) if err != nil { return PrivateKey{}, err } return DecodePrivateKey(string(bin)) } // scryptParamsFromLimits ports libsodium's pwhash_scryptsalsa208sha256 // pickparams logic so that scrypt parameters match what the minisign C // reference implementation uses for the same opslimit and memlimit. func scryptParamsFromLimits(opslimit, memlimit uint64) (N, r, p int, err error) { if opslimit < 32768 { opslimit = 32768 } r = 8 pick := func(maxN uint64) int { ln := 1 for ln < 63 && uint64(1)< 0x3fffffff { maxrp = 0x3fffffff } p = int(maxrp / uint64(r)) } if ln >= 63 { return 0, 0, 0, errors.New("Invalid scrypt parameters") } N = 1 << ln return N, r, p, nil } // Decrypt decrypts the secret key in place. It is a no-op on an unencrypted // key. The passphrase is verified against the stored Blake2b-256 checksum. func (sk *PrivateKey) Decrypt(password string) error { if !sk.IsEncrypted() { return nil } if string(sk.KDFAlgorithm[:]) != kdfAlgScrypt { return errors.New("Unsupported KDF algorithm") } N, r, p, err := scryptParamsFromLimits(sk.KDFOpsLimit, sk.KDFMemLimit) if err != nil { return err } stream, err := scrypt.Key([]byte(password), sk.KDFSalt[:], N, r, p, streamLen) if err != nil { return err } defer wipe(stream) var keyId [8]byte var secret [64]byte var chk [32]byte defer wipe(secret[:]) defer wipe(chk[:]) for i := range keyId { keyId[i] = sk.KeyId[i] ^ stream[i] } for i := range secret { secret[i] = sk.SecretKey[i] ^ stream[8+i] } for i := range chk { chk[i] = sk.Checksum[i] ^ stream[72+i] } h, _ := blake2b.New256(nil) h.Write(sk.SignatureAlgorithm[:]) h.Write(keyId[:]) h.Write(secret[:]) expected := h.Sum(nil) if subtle.ConstantTimeCompare(expected, chk[:]) != 1 { return errors.New("Wrong password") } sk.KeyId = keyId sk.SecretKey = secret sk.Checksum = chk sk.KDFAlgorithm = [2]byte{0, 0} return nil } // Wipe overwrites the in-memory secret key, checksum and key id with zeros. // Call this when the key is no longer needed; the buffer will not be usable // for signing afterwards. func (sk *PrivateKey) Wipe() { wipe(sk.SecretKey[:]) wipe(sk.Checksum[:]) wipe(sk.KeyId[:]) } func wipe(b []byte) { for i := range b { b[i] = 0 } } // isPrintable matches the C reference's is_printable so that trusted comments // we emit are accepted when read back. func isPrintable(s string) bool { for i := 0; i < len(s); i++ { c := s[i] if c == '\t' { continue } if c < 0x20 || c >= 0x7f { return false } } return true } // PublicKey derives the public key from the (decrypted) secret key. func (sk *PrivateKey) PublicKey() PublicKey { var pk PublicKey pk.SignatureAlgorithm = [2]byte{'E', 'd'} pk.KeyId = sk.KeyId copy(pk.PublicKey[:], sk.SecretKey[32:64]) return pk } // SignOptions configures a signing operation. type SignOptions struct { // UntrustedComment is written as the first line of the .minisig file. // Empty defaults to "signature from minisign secret key". UntrustedComment string // TrustedComment is signed as part of the global signature. // Empty defaults to "timestamp:". TrustedComment string // Hashed selects the prehashed signature variant ("ED"), in which the // signature is computed over a Blake2b-512 hash of the message. This is // the recommended mode and is required for streaming signers. When // false, the signature is computed directly over the message bytes // (legacy "Ed" mode). Hashed bool } // Sign produces a minisign signature over data. The returned Signature can be // serialized with Encode. func (sk *PrivateKey) Sign(data []byte, opts SignOptions) (Signature, error) { if opts.Hashed { h, _ := blake2b.New512(nil) h.Write(data) return sk.signRaw(h.Sum(nil), true, opts) } return sk.signRaw(data, false, opts) } func (sk *PrivateKey) signRaw(message []byte, hashed bool, opts SignOptions) (Signature, error) { if sk.IsEncrypted() { return Signature{}, errors.New("Secret key is encrypted; call Decrypt first") } if string(sk.SignatureAlgorithm[:]) != sigAlgEd { return Signature{}, errors.New("Unsupported signature algorithm") } untrusted := opts.UntrustedComment if untrusted == "" { untrusted = defaultSigUntrusted } if hasNewline(untrusted) { return Signature{}, errors.New("Untrusted comment must fit on a single line") } trusted := opts.TrustedComment if trusted == "" { trusted = fmt.Sprintf("timestamp:%d", time.Now().Unix()) } if hasNewline(trusted) { return Signature{}, errors.New("Trusted comment must fit on a single line") } if !isPrintable(trusted) { return Signature{}, errors.New("Trusted comment contains unprintable characters") } if len(trusted) > trustedCommentMaxLen { return Signature{}, errors.New("Trusted comment too long") } var sig Signature sig.KeyId = sk.KeyId sig.UntrustedComment = commentPrefix + untrusted sig.TrustedComment = trustedPrefix + trusted if hashed { sig.SignatureAlgorithm = [2]byte{'E', 'D'} } else { sig.SignatureAlgorithm = [2]byte{'E', 'd'} } edSK := ed25519.PrivateKey(sk.SecretKey[:]) raw := ed25519.Sign(edSK, message) copy(sig.Signature[:], raw) global := make([]byte, 0, len(raw)+len(trusted)) global = append(global, raw...) global = append(global, trusted...) copy(sig.GlobalSignature[:], ed25519.Sign(edSK, global)) return sig, nil } // SignFile signs the contents of a file. In Hashed mode the file is streamed // through Blake2b-512 with constant memory; in legacy mode the file is loaded // into memory because Ed25519 needs the message twice. // // If opts.TrustedComment is empty, a default of "timestamp:\tfile:" // (with "\thashed" appended for prehashed signatures) is used — matching what // the reference minisign(1) CLI writes. func (sk *PrivateKey) SignFile(file string, opts SignOptions) (Signature, error) { if opts.TrustedComment == "" { opts.TrustedComment = defaultTrustedComment(filepath.Base(file), opts.Hashed) } if !opts.Hashed { data, err := os.ReadFile(file) if err != nil { return Signature{}, err } return sk.signRaw(data, false, opts) } f, err := os.Open(file) if err != nil { return Signature{}, err } defer f.Close() h, _ := blake2b.New512(nil) if _, err := io.Copy(h, f); err != nil { return Signature{}, err } return sk.signRaw(h.Sum(nil), true, opts) } func defaultTrustedComment(basename string, hashed bool) string { suffix := "" if hashed { suffix = "\thashed" } return fmt.Sprintf("timestamp:%d\tfile:%s%s", time.Now().Unix(), basename, suffix) } // Encode serializes the signature into the textual minisign .minisig format. func (sig Signature) Encode() []byte { bin1 := make([]byte, 0, 74) bin1 = append(bin1, sig.SignatureAlgorithm[:]...) bin1 = append(bin1, sig.KeyId[:]...) bin1 = append(bin1, sig.Signature[:]...) untrusted := sig.UntrustedComment if untrusted == "" { untrusted = commentPrefix + defaultSigUntrusted } else if !strings.HasPrefix(untrusted, commentPrefix) { untrusted = commentPrefix + untrusted } trusted := sig.TrustedComment if !strings.HasPrefix(trusted, trustedPrefix) { trusted = trustedPrefix + trusted } var buf bytes.Buffer buf.WriteString(untrusted) buf.WriteByte('\n') buf.WriteString(base64.StdEncoding.EncodeToString(bin1)) buf.WriteByte('\n') buf.WriteString(trusted) buf.WriteByte('\n') buf.WriteString(base64.StdEncoding.EncodeToString(sig.GlobalSignature[:])) buf.WriteByte('\n') return buf.Bytes() } // MarshalText implements encoding.TextMarshaler. func (sig Signature) MarshalText() ([]byte, error) { return sig.Encode(), nil } func hasNewline(s string) bool { return strings.ContainsAny(s, "\r\n") } jedisct1-go-minisign-4975600/sign_test.go000066400000000000000000000221461520562440700201760ustar00rootroot00000000000000package minisign import ( "bytes" "os" "os/exec" "path/filepath" "strings" "testing" ) // Unencrypted key generated by `minisign -G -W`. const ( testUnencryptedSK = `untrusted comment: minisign encrypted secret key RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOItWpGuGQbG4C9WXaxEYLgZ2xxuqfbuZmDgAhQ8Unot8t7SyxZ0nVh0gESesJ6Ay57fGFJ9T1ajVmanT7MFMCCDbPZ8uqDcSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= ` testUnencryptedPK = `untrusted comment: minisign public key B141866BA4568B38 RWQ4i1aka4ZBsR0gESesJ6Ay57fGFJ9T1ajVmanT7MFMCCDbPZ8uqDcS ` ) // Encrypted key generated by `minisign -G` with passphrase "testpass". const ( testEncryptedSK = `untrusted comment: minisign encrypted secret key RWRTY0IyxYlIT2FS5i8PqThE9swBemvY94JDIMqo75UBK3XO/aUAAAACAAAAAAAAAEAAAAAAUhlw8nsT1tuVUekS6Je3iUwoWFdb1xiLonO35G66RiVvM/QgrBtnDa0Dhbt7H3oYMh4aFLiNxMs24gzXqHVsvRVthMeF08fN8r6siRdBpiBZ36B7rox2lmYIYgg5T8qt7tOxo9doAxk= ` testEncryptedPK = `untrusted comment: minisign public key 9149E58DCF22FFC1 RWTB/yLPjeVJkXKtzk1nZI0TU+fZPqEaIzg1ABHwfnI8pZNWtifIpWBq ` testEncryptedPassphrase = "testpass" ) func TestDecodePrivateKeyUnencrypted(t *testing.T) { sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } if sk.IsEncrypted() { t.Fatal("expected unencrypted key") } pk, err := DecodePublicKey(testUnencryptedPK) if err != nil { t.Fatal(err) } if sk.PublicKey().PublicKey != pk.PublicKey { t.Fatal("derived public key does not match") } if sk.PublicKey().KeyId != pk.KeyId { t.Fatal("key ids do not match") } } func TestDecryptPrivateKey(t *testing.T) { sk, err := DecodePrivateKey(testEncryptedSK) if err != nil { t.Fatal(err) } if !sk.IsEncrypted() { t.Fatal("expected encrypted key") } if err := sk.Decrypt(testEncryptedPassphrase); err != nil { t.Fatal(err) } if sk.IsEncrypted() { t.Fatal("key still flagged as encrypted") } pk, err := DecodePublicKey(testEncryptedPK) if err != nil { t.Fatal(err) } if sk.PublicKey().PublicKey != pk.PublicKey { t.Fatal("derived public key does not match") } } func TestDecryptWrongPassword(t *testing.T) { sk, err := DecodePrivateKey(testEncryptedSK) if err != nil { t.Fatal(err) } if err := sk.Decrypt("nope"); err == nil { t.Fatal("expected error for wrong password") } } func TestSignRoundTripHashed(t *testing.T) { signRoundTrip(t, true) } func TestSignRoundTripLegacy(t *testing.T) { signRoundTrip(t, false) } func signRoundTrip(t *testing.T, hashed bool) { t.Helper() sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } pk, err := DecodePublicKey(testUnencryptedPK) if err != nil { t.Fatal(err) } data := []byte("the quick brown fox jumps over the lazy dog") sig, err := sk.Sign(data, SignOptions{ UntrustedComment: "hello world", TrustedComment: "file:fox.txt", Hashed: hashed, }) if err != nil { t.Fatal(err) } parsed, err := DecodeSignature(string(sig.Encode())) if err != nil { t.Fatalf("DecodeSignature: %v", err) } ok, err := pk.Verify(data, parsed) if err != nil { t.Fatal(err) } if !ok { t.Fatal("verification failed") } if parsed.UntrustedComment != "untrusted comment: hello world" { t.Fatalf("unexpected untrusted comment: %q", parsed.UntrustedComment) } if parsed.TrustedComment != "trusted comment: file:fox.txt" { t.Fatalf("unexpected trusted comment: %q", parsed.TrustedComment) } } func TestSignDefaults(t *testing.T) { sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } sig, err := sk.Sign([]byte("hi"), SignOptions{}) if err != nil { t.Fatal(err) } if !strings.HasPrefix(sig.UntrustedComment, "untrusted comment: signature from minisign secret key") { t.Fatalf("unexpected default untrusted comment: %q", sig.UntrustedComment) } if !strings.HasPrefix(sig.TrustedComment, "trusted comment: timestamp:") { t.Fatalf("unexpected default trusted comment: %q", sig.TrustedComment) } } func TestSignRejectsMultilineComments(t *testing.T) { sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } if _, err := sk.Sign([]byte("x"), SignOptions{UntrustedComment: "a\nb"}); err == nil { t.Fatal("expected error for multiline untrusted comment") } if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: "a\nb"}); err == nil { t.Fatal("expected error for multiline trusted comment") } } func TestSignRejectsUnprintableTrustedComment(t *testing.T) { sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: "foo\x01bar"}); err == nil { t.Fatal("expected error for unprintable trusted comment") } } func TestSignRejectsOverlongTrustedComment(t *testing.T) { sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } long := strings.Repeat("a", 8200) if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: long}); err == nil { t.Fatal("expected error for overlong trusted comment") } } func TestSignFileStreamHashedInterop(t *testing.T) { bin, err := exec.LookPath("minisign") if err != nil { t.Skip("minisign binary not available") } sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } dir := t.TempDir() msgPath := filepath.Join(dir, "big.bin") if err := os.WriteFile(msgPath, bytes.Repeat([]byte("the quick brown fox\n"), 4096), 0o644); err != nil { t.Fatal(err) } pubPath := filepath.Join(dir, "pk.pub") if err := os.WriteFile(pubPath, []byte(testUnencryptedPK), 0o644); err != nil { t.Fatal(err) } sig, err := sk.SignFile(msgPath, SignOptions{Hashed: true}) if err != nil { t.Fatal(err) } if !strings.Contains(sig.TrustedComment, "file:big.bin") { t.Fatalf("expected default trusted comment to include file:big.bin, got %q", sig.TrustedComment) } if !strings.Contains(sig.TrustedComment, "hashed") { t.Fatalf("expected default trusted comment to include hashed marker, got %q", sig.TrustedComment) } if err := os.WriteFile(msgPath+".minisig", sig.Encode(), 0o644); err != nil { t.Fatal(err) } out, err := exec.Command(bin, "-Vm", msgPath, "-p", pubPath).CombinedOutput() if err != nil { t.Fatalf("minisign verify failed: %v\n%s", err, out) } } func TestSignRequiresDecryptedKey(t *testing.T) { sk, err := DecodePrivateKey(testEncryptedSK) if err != nil { t.Fatal(err) } if _, err := sk.Sign([]byte("x"), SignOptions{}); err == nil { t.Fatal("expected error signing with encrypted key") } } // TestInteropMinisignCLI signs data with go-minisign and verifies the result // using the real minisign(1) binary, exercising both legacy and prehashed // modes. The test is skipped when the binary is unavailable. func TestInteropMinisignCLI(t *testing.T) { bin, err := exec.LookPath("minisign") if err != nil { t.Skip("minisign binary not available") } sk, err := DecodePrivateKey(testUnencryptedSK) if err != nil { t.Fatal(err) } dir := t.TempDir() pubPath := filepath.Join(dir, "test.pub") if err := os.WriteFile(pubPath, []byte(testUnencryptedPK), 0o644); err != nil { t.Fatal(err) } for _, hashed := range []bool{true, false} { name := "legacy" if hashed { name = "hashed" } t.Run(name, func(t *testing.T) { msgPath := filepath.Join(dir, "msg-"+name+".txt") sigPath := msgPath + ".minisig" msg := []byte("minisign interop test " + name) if err := os.WriteFile(msgPath, msg, 0o644); err != nil { t.Fatal(err) } sig, err := sk.Sign(msg, SignOptions{ TrustedComment: "interop test " + name, UntrustedComment: "produced by go-minisign", Hashed: hashed, }) if err != nil { t.Fatal(err) } if err := os.WriteFile(sigPath, sig.Encode(), 0o644); err != nil { t.Fatal(err) } out, err := exec.Command(bin, "-Vm", msgPath, "-p", pubPath).CombinedOutput() if err != nil { t.Fatalf("minisign verify failed: %v\n%s", err, out) } }) } } // TestReverseInteropFromCLI signs with the minisign CLI and verifies through // go-minisign so that signature parsing tolerates the canonical formatting. func TestReverseInteropFromCLI(t *testing.T) { bin, err := exec.LookPath("minisign") if err != nil { t.Skip("minisign binary not available") } dir := t.TempDir() skPath := filepath.Join(dir, "sk.key") pkPath := filepath.Join(dir, "pk.pub") if err := os.WriteFile(skPath, []byte(testUnencryptedSK), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(pkPath, []byte(testUnencryptedPK), 0o644); err != nil { t.Fatal(err) } msgPath := filepath.Join(dir, "msg.txt") msg := []byte("signed with the cli") if err := os.WriteFile(msgPath, msg, 0o644); err != nil { t.Fatal(err) } cmd := exec.Command(bin, "-S", "-s", skPath, "-m", msgPath, "-t", "from CLI") cmd.Stdin = strings.NewReader("\n") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("minisign sign: %v\n%s", err, out) } sigBytes, err := os.ReadFile(msgPath + ".minisig") if err != nil { t.Fatal(err) } sig, err := DecodeSignature(string(sigBytes)) if err != nil { t.Fatal(err) } pk, err := DecodePublicKey(testUnencryptedPK) if err != nil { t.Fatal(err) } ok, err := pk.Verify(msg, sig) if err != nil { t.Fatal(err) } if !ok { t.Fatal("verification failed") } }