Skip to content

Commit c05e17b

Browse files
committed
openpgp/clearsign: reject potentially misleading headers and messages
Aida Mynzhasova of SEC Consult Vulnerability Lab reported that the clearsign package accepts some malformed messages, which can make it possible for an attacker to trick a human user (but not a Go program) into thinking unverified text is part of the message. For example, if in the following message the vertical tab character is printed as a new line, a human observer could believe that the unverified header text is part of the signed message. -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1\x0b\x0bThis text is part of the header. Hello world! -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) iJwEAQECAAYFAk8kMuEACgkQO9o98PRieSpMsAQAhmY/vwmNpflrPgmfWsYhk5O8 [...] MyTpno24AjIAGb+mH1U= =hIJ6 -----END PGP SIGNATURE----- The OpenPGP specs are delightfully vague about purpose and validation of these headers. RFC 4880, Section 7 says The cleartext signed message consists of: - The cleartext header '-----BEGIN PGP SIGNED MESSAGE-----' on a single line, - One or more "Hash" Armor Headers, - Exactly one empty line not included into the message digest, [...] but also If MD5 is the only hash used, then an implementation MAY omit this header for improved V2.x compatibility. and If more than one message digest is used in the signature, the "Hash" armor header contains a comma-delimited list of used message digests. which seems to suggest that there can be zero or more Hash headers, each with one or more algorithms, and no other header types. Anyway, it's entirely unclear what security purpose, if any, the Hash header accomplishes. If the hash is too weak to be secure or unsupported, the verification will fail. Otherwise, the user shouldn't care. Given its dubious function, avoid breaking abstractions to check that it matches the signature, and just document it as unverified. As for valid characters, RFC 4880 is silent, except reluctantly mentioning that the Comment header can be UTF-8, but I am going to assume that all hash algorithms will have ASCII names, because come on. Even more importantly, reject non-Hash SIGNED MESSAGE headers (as opposed to the SIGNATURE headers), to prevent a "Thank you!" message turning into -----BEGIN PGP SIGNED MESSAGE----- Reminder: I need you to wire $100k to 12345566 as soon as possible. Thank you! -----BEGIN PGP SIGNATURE----- [...] While at it, also check for trailing characters after the signed message delimiter, as they are invalid and can be similarly used to confuse humans. The Decode API is also unfortunate in that it doesn't return an error, so we can't tell the user what's wrong with the message, but that's what we've got. Change-Id: I8a72c4851075337443d7a27e0b49a6b6e39f5a41 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/453011 Reviewed-by: Adam Langley <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/crypto/+/173778 Run-TryBot: Filippo Valsorda <[email protected]> Reviewed-by: Adam Langley <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent d864b10 commit c05e17b

File tree

2 files changed

+85
-22
lines changed

2 files changed

+85
-22
lines changed

Diff for: openpgp/clearsign/clearsign.go

+28-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"io"
1919
"net/textproto"
2020
"strconv"
21+
"strings"
2122

2223
"golang.org/x/crypto/openpgp/armor"
2324
"golang.org/x/crypto/openpgp/errors"
@@ -27,7 +28,7 @@ import (
2728
// A Block represents a clearsigned message. A signature on a Block can
2829
// be checked by passing Bytes into openpgp.CheckDetachedSignature.
2930
type Block struct {
30-
Headers textproto.MIMEHeader // Optional message headers
31+
Headers textproto.MIMEHeader // Optional unverified Hash headers
3132
Plaintext []byte // The original message text
3233
Bytes []byte // The signed message
3334
ArmoredSignature *armor.Block // The signature block
@@ -69,8 +70,13 @@ func getLine(data []byte) (line, rest []byte) {
6970
return data[0:i], data[j:]
7071
}
7172

72-
// Decode finds the first clearsigned message in data and returns it, as well
73-
// as the suffix of data which remains after the message.
73+
// Decode finds the first clearsigned message in data and returns it, as well as
74+
// the suffix of data which remains after the message. Any prefix data is
75+
// discarded.
76+
//
77+
// If no message is found, or if the message is invalid, Decode returns nil and
78+
// the whole data slice. The only allowed header type is Hash, and it is not
79+
// verified against the signature hash.
7480
func Decode(data []byte) (b *Block, rest []byte) {
7581
// start begins with a newline. However, at the very beginning of
7682
// the byte array, we'll accept the start string without it.
@@ -83,8 +89,11 @@ func Decode(data []byte) (b *Block, rest []byte) {
8389
return nil, data
8490
}
8591

86-
// Consume the start line.
87-
_, rest = getLine(rest)
92+
// Consume the start line and check it does not have a suffix.
93+
suffix, rest := getLine(rest)
94+
if len(suffix) != 0 {
95+
return nil, data
96+
}
8897

8998
var line []byte
9099
b = &Block{
@@ -103,15 +112,25 @@ func Decode(data []byte) (b *Block, rest []byte) {
103112
break
104113
}
105114

115+
// Reject headers with control or Unicode characters.
116+
if i := bytes.IndexFunc(line, func(r rune) bool {
117+
return r < 0x20 || r > 0x7e
118+
}); i != -1 {
119+
return nil, data
120+
}
121+
106122
i := bytes.Index(line, []byte{':'})
107123
if i == -1 {
108124
return nil, data
109125
}
110126

111-
key, val := line[0:i], line[i+1:]
112-
key = bytes.TrimSpace(key)
113-
val = bytes.TrimSpace(val)
114-
b.Headers.Add(string(key), string(val))
127+
key, val := string(line[0:i]), string(line[i+1:])
128+
key = strings.TrimSpace(key)
129+
if key != "Hash" {
130+
return nil, data
131+
}
132+
val = strings.TrimSpace(val)
133+
b.Headers.Add(key, val)
115134
}
116135

117136
firstLine := true

Diff for: openpgp/clearsign/clearsign_test.go

+57-13
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,6 @@ func TestParse(t *testing.T) {
4747
testParse(t, clearsignInput2, "\r\n\r\n(This message has a couple of blank lines at the start and end.)\r\n\r\n", "\n\n(This message has a couple of blank lines at the start and end.)\n\n\n")
4848
}
4949

50-
func TestParseInvalid(t *testing.T) {
51-
if b, _ := Decode(clearsignInput3); b != nil {
52-
t.Fatal("decoded a bad clearsigned message without any error")
53-
}
54-
}
55-
5650
func TestParseWithNoNewlineAtEnd(t *testing.T) {
5751
input := clearsignInput
5852
input = input[:len(input)-len("trailing")-1]
@@ -140,6 +134,10 @@ func (qr *quickRand) Read(p []byte) (int, error) {
140134
}
141135

142136
func TestMultiSign(t *testing.T) {
137+
if testing.Short() {
138+
t.Skip("skipping long test in -short mode")
139+
}
140+
143141
zero := quickRand(0)
144142
config := packet.Config{Rand: &zero}
145143

@@ -193,6 +191,59 @@ func TestMultiSign(t *testing.T) {
193191
}
194192
}
195193

194+
const signatureBlock = `
195+
-----BEGIN PGP SIGNATURE-----
196+
Version: OpenPrivacy 0.99
197+
198+
yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS
199+
vBSFjNSiVHsuAA==
200+
=njUN
201+
-----END PGP SIGNATURE-----
202+
`
203+
204+
var invalidInputs = []string{
205+
`
206+
-----BEGIN PGP SIGNED MESSAGE-----
207+
Hash: SHA256
208+
209+
(This message was truncated.)
210+
`,
211+
`
212+
-----BEGIN PGP SIGNED MESSAGE-----garbage
213+
Hash: SHA256
214+
215+
_o/
216+
` + signatureBlock,
217+
`
218+
garbage-----BEGIN PGP SIGNED MESSAGE-----
219+
Hash: SHA256
220+
221+
_o/
222+
` + signatureBlock,
223+
`
224+
-----BEGIN PGP SIGNED MESSAGE-----
225+
Hash: SHA` + "\x0b\x0b" + `256
226+
227+
_o/
228+
` + signatureBlock,
229+
`
230+
-----BEGIN PGP SIGNED MESSAGE-----
231+
NotHash: SHA256
232+
233+
_o/
234+
` + signatureBlock,
235+
}
236+
237+
func TestParseInvalid(t *testing.T) {
238+
for i, input := range invalidInputs {
239+
if b, rest := Decode([]byte(input)); b != nil {
240+
t.Errorf("#%d: decoded a bad clearsigned message without any error", i)
241+
} else if string(rest) != input {
242+
t.Errorf("#%d: did not return all data with a bad message", i)
243+
}
244+
}
245+
}
246+
196247
var clearsignInput = []byte(`
197248
;lasjlkfdsa
198249
@@ -235,13 +286,6 @@ qZg6BaTvOxepqOxnhVU=
235286
236287
trailing`)
237288

238-
var clearsignInput3 = []byte(`
239-
-----BEGIN PGP SIGNED MESSAGE-----
240-
Hash: SHA256
241-
242-
(This message was truncated.)
243-
`)
244-
245289
var signingKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
246290
Version: GnuPG v1.4.10 (GNU/Linux)
247291

0 commit comments

Comments
 (0)