Skip to content

Commit 0814912

Browse files
committed
improve docs and implementation of Signature, PrivateKey and PublicKey
This commit improves the documentation for the core minisign types and improves the implementation for various functions. It also adds an additional test vector for (un)marshaling private keys. Signed-off-by: Andreas Auernhammer <[email protected]>
1 parent 6d740d9 commit 0814912

File tree

8 files changed

+177
-88
lines changed

8 files changed

+177
-88
lines changed

.golangci.yml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
linters-settings:
2-
golint:
3-
min-confidence: 0
4-
52
misspell:
63
locale: US
74

@@ -11,24 +8,27 @@ linters-settings:
118
linters:
129
disable-all: true
1310
enable:
14-
- typecheck
11+
- durationcheck
12+
- gocritic
13+
- gofmt
1514
- goimports
16-
- misspell
17-
- staticcheck
15+
- gomodguard
1816
- govet
19-
- revive
2017
- ineffassign
21-
- gosimple
22-
- unused
23-
- prealloc
18+
- misspell
19+
- revive
20+
- staticcheck
21+
- tenv
22+
- typecheck
2423
- unconvert
25-
- gofumpt
24+
- unused
2625

2726
issues:
2827
exclude-use-default: false
2928
exclude:
30-
- "var-naming: don't use ALL_CAPS in Go names; use CamelCase"
3129
- "package-comments: should have a package comment"
30+
- "exitAfterDefer:"
31+
- "captLocal:"
3232

3333
service:
34-
golangci-lint-version: 1.48.0 # use the fixed version to not introduce new linters unexpectedly
34+
golangci-lint-version: 1.57.2 # use the fixed version to not introduce new linters unexpectedly

internal/testdata/minisign_unencrypted.key

Lines changed: 0 additions & 2 deletions
This file was deleted.

internal/testdata/unencrypted.key

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
untrusted comment: minisign encrypted secret key
2+
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbuUYgQpHKDcmmMQj9cgqohWX321PrXUDFfCVWOXDZp8kLw2/qju66KnI28LcOaA7ZywNP5vDVtlHeyzit3lxeqirS5+2UImrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
3+
4+
untrusted comment: minisign encrypted secret key
5+
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb/yydu4x5dcvbgaLZRtY5v8wFvgzMkvKyALUXUWcT+bvaqFvuvkUyUfMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

minisign.pub

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
untrusted comment: minisign public key D7E531EE76B2FC6F
2+
RWRv/LJ27jHl10fMd7ozqYIs8zOaPqWf6EjnWSqkOpOQiD1UJpOgCFm0

private.go

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ func (p PrivateKey) Equal(x crypto.PrivateKey) bool {
106106
//
107107
// For password-protected private keys refer to [EncryptKey].
108108
func (p PrivateKey) MarshalText() ([]byte, error) {
109+
// A non-encrypted private key has the same format as an encrypted one.
110+
// However, the salt, and auth. tag are set to all zero.
109111
var b [privateKeySize]byte
110112

111113
binary.LittleEndian.PutUint16(b[:], EdDSA)
@@ -115,6 +117,8 @@ func (p PrivateKey) MarshalText() ([]byte, error) {
115117
binary.LittleEndian.PutUint64(b[54:], p.id)
116118
copy(b[62:], p.bytes[:])
117119

120+
// It seems odd that the comment says: "encrypted secret key".
121+
// However, the original C implementation behaves like this.
118122
const comment = "untrusted comment: minisign encrypted secret key\n"
119123
encodedBytes := make([]byte, len(comment)+base64.StdEncoding.EncodedLen(len(b)))
120124
copy(encodedBytes, []byte(comment))
@@ -140,7 +144,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error {
140144
}
141145

142146
var (
143-
empty [32]byte
147+
empty [32]byte // For checking that the salt/tag are empty
144148

145149
kType = binary.LittleEndian.Uint16(b)
146150
kdf = binary.LittleEndian.Uint16(b[2:])
@@ -149,7 +153,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error {
149153
scryptOps = binary.LittleEndian.Uint64(b[38:])
150154
scryptMem = binary.LittleEndian.Uint64(b[46:])
151155
key = b[54:126]
152-
checksum = b[126:privateKeySize]
156+
tag = b[126:privateKeySize]
153157
)
154158
if kType != EdDSA {
155159
return fmt.Errorf("minisign: invalid private key: invalid key type '%d'", kType)
@@ -163,7 +167,7 @@ func (p *PrivateKey) UnmarshalText(text []byte) error {
163167
if hType != algorithmBlake2b {
164168
return fmt.Errorf("minisign: invalid private key: invalid hash type '%d'", hType)
165169
}
166-
if !bytes.Equal(salt[:], empty[:]) {
170+
if !bytes.Equal(salt, empty[:]) {
167171
return errors.New("minisign: invalid private key: salt is not empty")
168172
}
169173
if scryptOps != 0 {
@@ -172,11 +176,11 @@ func (p *PrivateKey) UnmarshalText(text []byte) error {
172176
if scryptMem != 0 {
173177
return errors.New("minisign: invalid private key: scrypt mem parameter is not zero")
174178
}
175-
if !bytes.Equal(checksum, empty[:]) {
179+
if !bytes.Equal(tag, empty[:]) {
176180
return errors.New("minisign: invalid private key: salt is not empty")
177181
}
178182

179-
p.id = binary.LittleEndian.Uint64(key[:8])
183+
p.id = binary.LittleEndian.Uint64(key)
180184
copy(p.bytes[:], key[8:])
181185
return nil
182186
}
@@ -235,10 +239,7 @@ func IsEncrypted(privateKey []byte) bool {
235239
}
236240
bytes = bytes[:n]
237241

238-
if len(bytes) != privateKeySize {
239-
return false
240-
}
241-
return binary.LittleEndian.Uint16(bytes[2:4]) == algorithmScrypt
242+
return len(bytes) >= 4 && binary.LittleEndian.Uint16(bytes[2:]) == algorithmScrypt
242243
}
243244

244245
var errDecrypt = errors.New("minisign: decryption failed")
@@ -247,47 +248,50 @@ var errDecrypt = errors.New("minisign: decryption failed")
247248
// the given password.
248249
func DecryptKey(password string, privateKey []byte) (PrivateKey, error) {
249250
privateKey = trimUntrustedComment(privateKey)
250-
bytes := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey)))
251-
n, err := base64.StdEncoding.Decode(bytes, privateKey)
251+
b := make([]byte, base64.StdEncoding.DecodedLen(len(privateKey)))
252+
n, err := base64.StdEncoding.Decode(b, privateKey)
252253
if err != nil {
253254
return PrivateKey{}, err
254255
}
255-
bytes = bytes[:n]
256+
b = b[:n]
256257

257-
if len(bytes) != privateKeySize {
258+
if len(b) != privateKeySize {
258259
return PrivateKey{}, errDecrypt
259260
}
260-
if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA {
261+
var (
262+
kType = binary.LittleEndian.Uint16(b)
263+
kdf = binary.LittleEndian.Uint16(b[2:])
264+
hType = binary.LittleEndian.Uint16(b[4:])
265+
salt = b[6:38]
266+
scryptOps = binary.LittleEndian.Uint64(b[38:])
267+
scryptMem = binary.LittleEndian.Uint64(b[46:])
268+
ciphertext = b[54:]
269+
)
270+
if kType != EdDSA {
261271
return PrivateKey{}, errDecrypt
262272
}
263-
if a := binary.LittleEndian.Uint16(bytes[2:4]); a != algorithmScrypt {
273+
if kdf != algorithmScrypt {
264274
return PrivateKey{}, errDecrypt
265275
}
266-
if a := binary.LittleEndian.Uint16(bytes[4:6]); a != algorithmBlake2b {
276+
if hType != algorithmBlake2b {
267277
return PrivateKey{}, errDecrypt
268278
}
269-
270-
var (
271-
scryptOps = binary.LittleEndian.Uint64(bytes[38:46])
272-
scryptMem = binary.LittleEndian.Uint64(bytes[46:54])
273-
)
274279
if scryptOps > scryptOpsLimit {
275280
return PrivateKey{}, errDecrypt
276281
}
277282
if scryptMem > scryptMemLimit {
278283
return PrivateKey{}, errDecrypt
279284
}
280-
var salt [32]byte
281-
copy(salt[:], bytes[6:38])
282-
privateKeyBytes, err := decryptKey(password, salt[:], scryptOps, scryptMem, bytes[54:])
285+
286+
plaintext, err := decryptKey(password, salt, scryptOps, scryptMem, ciphertext)
283287
if err != nil {
284288
return PrivateKey{}, err
285289
}
286290

287291
key := PrivateKey{
288-
id: binary.LittleEndian.Uint64(privateKeyBytes[:8]),
292+
id: binary.LittleEndian.Uint64(plaintext),
289293
}
290-
copy(key.bytes[:], privateKeyBytes[8:])
294+
copy(key.bytes[:], plaintext[8:])
291295
return key, nil
292296
}
293297

@@ -368,7 +372,7 @@ func decryptKey(password string, salt []byte, ops, mem uint64, ciphertext []byte
368372
binary.LittleEndian.PutUint16(message[:2], EdDSA)
369373
copy(message[2:], privateKeyBytes)
370374

371-
if sum := blake2b.Sum256(message[:]); subtle.ConstantTimeCompare(sum[:], checksum[:]) != 1 {
375+
if sum := blake2b.Sum256(message[:]); subtle.ConstantTimeCompare(sum[:], checksum) != 1 {
372376
return nil, errDecrypt
373377
}
374378
return privateKeyBytes, nil

private_test.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,88 @@ package minisign
66

77
import (
88
"bytes"
9+
"encoding/base64"
910
"os"
11+
"strconv"
1012
"testing"
1113
)
1214

15+
var marshalPrivateKeyTests = []struct {
16+
ID uint64
17+
Bytes []byte
18+
}{
19+
{
20+
ID: htoi("3728470A8118E56E"),
21+
Bytes: b64("JpjEI/XIKqIVl99tT611AxXwlVjlw2afJC8Nv6o7uuipyNvC3DmgO2csDT+bw1bZR3ss4rd5cXqoq0uftlCJqw=="),
22+
},
23+
{
24+
ID: htoi("D7E531EE76B2FC6F"),
25+
Bytes: b64("L24Gi2UbWOb/MBb4MzJLysgC1F1FnE/m72qhb7r5FMlHzHe6M6mCLPMzmj6ln+hI51kqpDqTkIg9VCaToAhZtA=="),
26+
},
27+
}
28+
29+
func TestPrivateKey_Marshal(t *testing.T) {
30+
raw, err := os.ReadFile("./internal/testdata/unencrypted.key")
31+
if err != nil {
32+
t.Fatalf("Failed to read private key: %v", err)
33+
}
34+
raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'})
35+
raw = bytes.TrimSuffix(raw, []byte{'\n'})
36+
37+
keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline
38+
if len(keys) != len(marshalPrivateKeyTests) {
39+
t.Fatalf("Test vectors don't match: got %d - want %d", len(marshalPrivateKeyTests), len(keys))
40+
}
41+
for i, test := range marshalPrivateKeyTests {
42+
key := PrivateKey{
43+
id: test.ID,
44+
}
45+
copy(key.bytes[:], test.Bytes)
46+
47+
text, err := key.MarshalText()
48+
if err != nil {
49+
t.Fatalf("Test %d: failed to marshal private key: %v", i, err)
50+
}
51+
if !bytes.Equal(text, keys[i]) {
52+
t.Log(len(text), len(keys[i]))
53+
t.Log(string(keys[i][len(keys[i])-1]))
54+
t.Fatalf("Test %d: failed to marshal private key:\nGot: %v\nWant: %v\n", i, text, keys[i])
55+
}
56+
}
57+
}
58+
1359
func TestPrivateKey_Unmarshal(t *testing.T) {
14-
raw, err := os.ReadFile("./internal/testdata/minisign_unencrypted.key")
60+
raw, err := os.ReadFile("./internal/testdata/unencrypted.key")
1561
if err != nil {
1662
t.Fatalf("Failed to read private key: %v", err)
1763
}
64+
raw = bytes.ReplaceAll(raw, []byte{'\r', '\n'}, []byte{'\n'})
65+
raw = bytes.TrimSuffix(raw, []byte{'\n'})
1866

19-
keys := bytes.Split(raw, []byte("\n\n")) // Private keys are separated by a newline
67+
keys := bytes.Split(raw, []byte{'\n', '\n'}) // Private keys are separated by a newline
2068
for _, k := range keys {
2169
var key PrivateKey
2270
if err := key.UnmarshalText(k); err != nil {
2371
t.Fatalf("Failed to unmarshal private key: %v\nPrivate key:\n%s", err, string(k))
2472
}
73+
74+
// Print test vector for marshaling:
75+
// t.Logf("\n{\n\tID: htoi(\"%X\"),\n\tBytes: b64(\"%s\"),\n}", key.id, base64.StdEncoding.EncodeToString(key.bytes[:]))
76+
}
77+
}
78+
79+
func htoi(s string) uint64 {
80+
i, err := strconv.ParseUint(s, 16, 64)
81+
if err != nil {
82+
panic(err)
83+
}
84+
return i
85+
}
86+
87+
func b64(s string) []byte {
88+
b, err := base64.StdEncoding.DecodeString(s)
89+
if err != nil {
90+
panic(err)
2591
}
92+
return b
2693
}

public.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import (
1616
"strings"
1717
)
1818

19-
// PublicKeyFromFile reads a new PublicKey from the
20-
// given file.
21-
func PublicKeyFromFile(path string) (PublicKey, error) {
22-
bytes, err := os.ReadFile(path)
19+
const publicKeySize = 2 + 8 + ed25519.PublicKeySize
20+
21+
// PublicKeyFromFile reads a PublicKey from the given file.
22+
func PublicKeyFromFile(filename string) (PublicKey, error) {
23+
bytes, err := os.ReadFile(filename)
2324
if err != nil {
2425
return PublicKey{}, err
2526
}
@@ -57,7 +58,7 @@ func (p PublicKey) Equal(x crypto.PublicKey) bool {
5758

5859
// String returns a base64 string representation of the PublicKey p.
5960
func (p PublicKey) String() string {
60-
var bytes [2 + 8 + ed25519.PublicKeySize]byte
61+
var bytes [publicKeySize]byte
6162
binary.LittleEndian.PutUint16(bytes[:2], EdDSA)
6263
binary.LittleEndian.PutUint64(bytes[2:10], p.ID())
6364
copy(bytes[10:], p.bytes[:])
@@ -69,22 +70,27 @@ func (p PublicKey) String() string {
6970
//
7071
// It never returns an error.
7172
func (p PublicKey) MarshalText() ([]byte, error) {
72-
comment := "untrusted comment: minisign public key: " + strings.ToUpper(strconv.FormatUint(p.ID(), 16)) + "\n"
73-
return []byte(comment + p.String()), nil
73+
s := make([]byte, 0, 113) // Size of a public key in text format
74+
s = append(s, "untrusted comment: minisign public key: "...)
75+
s = append(s, strings.ToUpper(strconv.FormatUint(p.ID(), 16))...)
76+
s = append(s, '\n')
77+
s = append(s, p.String()...)
78+
return s, nil
7479
}
7580

76-
// UnmarshalText parses text as textual-encoded public key.
77-
// It returns an error if text is not a well-formed public key.
81+
// UnmarshalText decodes a textual representation of a public key into p.
82+
//
83+
// It returns an error in case of a malformed key.
7884
func (p *PublicKey) UnmarshalText(text []byte) error {
7985
text = trimUntrustedComment(text)
8086
bytes := make([]byte, base64.StdEncoding.DecodedLen(len(text)))
8187
n, err := base64.StdEncoding.Decode(bytes, text)
8288
if err != nil {
8389
return fmt.Errorf("minisign: invalid public key: %v", err)
8490
}
85-
bytes = bytes[:n] // Adjust b/c text may contain '\r' or '\n' which would have been ignored during decoding.
91+
bytes = bytes[:n] // Adjust since text may contain '\r' or '\n' which would have been ignored during decoding.
8692

87-
if n = len(bytes); n != 2+8+ed25519.PublicKeySize {
93+
if n = len(bytes); n != publicKeySize {
8894
return errors.New("minisign: invalid public key length " + strconv.Itoa(n))
8995
}
9096
if a := binary.LittleEndian.Uint16(bytes[:2]); a != EdDSA {

0 commit comments

Comments
 (0)