CVE-2016-9122
Description
go-jose before 1.0.4 suffers from multiple signatures exploitation. The go-jose library supports messages with multiple signatures. However, when validating a signed message the API did not indicate which signature was valid, which could potentially lead to confusion. For example, users of the library might mistakenly read protected header values from an attached signature that was different from the one originally validated.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
gopkg.in/square/go-jose.v1Go | < 1.1.0 | 1.1.0 |
Affected products
1- Range: Go JOSE All versions before 1.0.4
Patches
32c5656adca99Merge pull request #111 from square/cs/better-multi
5 files changed · +148 −41
crypter.go+72 −5 modified@@ -19,6 +19,7 @@ package jose import ( "crypto/ecdsa" "crypto/rsa" + "errors" "fmt" "reflect" ) @@ -292,10 +293,16 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JsonWe return obj, nil } -// Decrypt and validate the object and return the plaintext. +// Decrypt and validate the object and return the plaintext. Note that this +// function does not support multi-recipient, if you desire multi-recipient +// decryption use DecryptMulti instead. func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { headers := obj.mergedHeaders(nil) + if len(obj.recipients) > 1 { + return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one") + } + if len(headers.Crit) > 0 { return nil, fmt.Errorf("square/go-jose: unsupported crit header") } @@ -323,27 +330,87 @@ func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) authData := obj.computeAuthData() var plaintext []byte - for _, recipient := range obj.recipients { + recipient := obj.recipients[0] + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + } + + if plaintext == nil { + return nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if obj.protected.Zip != "" { + plaintext, err = decompress(obj.protected.Zip, plaintext) + } + + return plaintext, err +} + +// DecryptMulti decrypts and validates the object and returns the plaintexts, +// with support for multiple recipients. It returns the index of the recipient +// for which the decryption was successful, the merged headers for that recipient, +// and the plaintext. +func (obj JsonWebEncryption) DecryptMulti(decryptionKey interface{}) (int, JoseHeader, []byte, error) { + globalHeaders := obj.mergedHeaders(nil) + + if len(globalHeaders.Crit) > 0 { + return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported crit header") + } + + decrypter, err := newDecrypter(decryptionKey) + if err != nil { + return -1, JoseHeader{}, nil, err + } + + cipher := getContentCipher(globalHeaders.Enc) + if cipher == nil { + return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(globalHeaders.Enc)) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + index := -1 + var plaintext []byte + var headers rawHeader + + for i, recipient := range obj.recipients { recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) if err == nil { + index = i + headers = recipientHeaders break } } } - if plaintext == nil { - return nil, ErrCryptoFailure + if plaintext == nil || err != nil { + return -1, JoseHeader{}, nil, ErrCryptoFailure } // The "zip" header parameter may only be present in the protected header. if obj.protected.Zip != "" { plaintext, err = decompress(obj.protected.Zip, plaintext) } - return plaintext, err + return index, headers.sanitized(), plaintext, err }
crypter_test.go+18 −17 modified@@ -272,7 +272,7 @@ func TestMultiRecipientJWE(t *testing.T) { err = enc.AddRecipient(RSA_OAEP, &rsaTestKey.PublicKey) if err != nil { - t.Error("error when adding RSA recipient", err) + t.Fatal("error when adding RSA recipient", err) } sharedKey := []byte{ @@ -282,45 +282,46 @@ func TestMultiRecipientJWE(t *testing.T) { err = enc.AddRecipient(A256GCMKW, sharedKey) if err != nil { - t.Error("error when adding AES recipient: ", err) - return + t.Fatal("error when adding AES recipient: ", err) } input := []byte("Lorem ipsum dolor sit amet") obj, err := enc.Encrypt(input) if err != nil { - t.Error("error in encrypt: ", err) - return + t.Fatal("error in encrypt: ", err) } msg := obj.FullSerialize() parsed, err := ParseEncrypted(msg) if err != nil { - t.Error("error in parse: ", err) - return + t.Fatal("error in parse: ", err) } - output, err := parsed.Decrypt(rsaTestKey) + i, _, output, err := parsed.DecryptMulti(rsaTestKey) if err != nil { - t.Error("error on decrypt with RSA: ", err) - return + t.Fatal("error on decrypt with RSA: ", err) + } + + if i != 0 { + t.Fatal("recipient index should be 0 for RSA key") } if bytes.Compare(input, output) != 0 { - t.Error("Decrypted output does not match input: ", output, input) - return + t.Fatal("Decrypted output does not match input: ", output, input) } - output, err = parsed.Decrypt(sharedKey) + i, _, output, err = parsed.DecryptMulti(sharedKey) if err != nil { - t.Error("error on decrypt with AES: ", err) - return + t.Fatal("error on decrypt with AES: ", err) + } + + if i != 1 { + t.Fatal("recipient index should be 1 for shared key") } if bytes.Compare(input, output) != 0 { - t.Error("Decrypted output does not match input", output, input) - return + t.Fatal("Decrypted output does not match input", output, input) } }
jws.go+4 −1 modified@@ -41,7 +41,10 @@ type rawSignatureInfo struct { // JsonWebSignature represents a signed JWS object after parsing. type JsonWebSignature struct { - payload []byte + payload []byte + // Signatures attached to this object (may be more than one for multi-sig). + // Be careful about accessing these directly, prefer to use Verify() or + // VerifyMulti() to ensure that the data you're getting is verified. Signatures []Signature }
signing.go+37 −3 modified@@ -19,6 +19,7 @@ package jose import ( "crypto/ecdsa" "crypto/rsa" + "errors" "fmt" ) @@ -193,13 +194,46 @@ func (ctx *genericSigner) SetEmbedJwk(embed bool) { } // Verify validates the signature on the object and returns the payload. +// Note that this function does not support multi-signature, if you desire +// multi-sig verification use VerifyMulti instead. func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, error) { verifier, err := newVerifier(verificationKey) if err != nil { return nil, err } - for _, signature := range obj.Signatures { + if len(obj.Signatures) > 1 { + return nil, errors.New("square/go-jose: too many signatures in payload; expecting only one") + } + + signature := obj.Signatures[0] + headers := signature.mergedHeaders() + if len(headers.Crit) > 0 { + // Unsupported crit header + return nil, ErrCryptoFailure + } + + input := obj.computeAuthData(&signature) + alg := SignatureAlgorithm(headers.Alg) + err = verifier.verifyPayload(input, signature.Signature, alg) + if err == nil { + return obj.payload, nil + } + + return nil, ErrCryptoFailure +} + +// VerifyMulti validates (one of the multiple) signatures on the object and +// returns the index of the signature that was verified, along with the signature +// object and the payload. We return the signature and index to guarantee that +// callers are getting the verified value. +func (obj JsonWebSignature) VerifyMulti(verificationKey interface{}) (int, Signature, []byte, error) { + verifier, err := newVerifier(verificationKey) + if err != nil { + return -1, Signature{}, nil, err + } + + for i, signature := range obj.Signatures { headers := signature.mergedHeaders() if len(headers.Crit) > 0 { // Unsupported crit header @@ -210,9 +244,9 @@ func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, error) alg := SignatureAlgorithm(headers.Alg) err := verifier.verifyPayload(input, signature.Signature, alg) if err == nil { - return obj.payload, nil + return i, signature, obj.payload, nil } } - return nil, ErrCryptoFailure + return -1, Signature{}, nil, ErrCryptoFailure }
signing_test.go+17 −15 modified@@ -224,43 +224,45 @@ func TestMultiRecipientJWS(t *testing.T) { input := []byte("Lorem ipsum dolor sit amet") obj, err := signer.Sign(input) if err != nil { - t.Error("error on sign: ", err) - return + t.Fatal("error on sign: ", err) } _, err = obj.CompactSerialize() if err == nil { - t.Error("message with multiple recipient was compact serialized") + t.Fatal("message with multiple recipient was compact serialized") } msg := obj.FullSerialize() obj, err = ParseSigned(msg) if err != nil { - t.Error("error on parse: ", err) - return + t.Fatal("error on parse: ", err) } - output, err := obj.Verify(&rsaTestKey.PublicKey) + i, _, output, err := obj.VerifyMulti(&rsaTestKey.PublicKey) if err != nil { - t.Error("error on verify: ", err) - return + t.Fatal("error on verify: ", err) + } + + if i != 0 { + t.Fatal("signature index should be 0 for RSA key") } if bytes.Compare(output, input) != 0 { - t.Error("input/output do not match", output, input) - return + t.Fatal("input/output do not match", output, input) } - output, err = obj.Verify(sharedKey) + i, _, output, err = obj.VerifyMulti(sharedKey) if err != nil { - t.Error("error on verify: ", err) - return + t.Fatal("error on verify: ", err) + } + + if i != 1 { + t.Fatal("signature index should be 1 for EC key") } if bytes.Compare(output, input) != 0 { - t.Error("input/output do not match", output, input) - return + t.Fatal("input/output do not match", output, input) } }
789a4c4bd4c1Use uint64 for all size calculations, size checks
4 files changed · +18 −13
cipher/cbc_hmac.go+8 −8 modified@@ -82,7 +82,7 @@ func (ctx *cbcAEAD) Overhead() int { // Seal encrypts and authenticates the plaintext. func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { // Output buffer -- must take care not to mangle plaintext input. - ciphertext := make([]byte, len(plaintext)+ctx.Overhead())[:len(plaintext)] + ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] copy(ciphertext, plaintext) ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) @@ -91,7 +91,7 @@ func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { cbc.CryptBlocks(ciphertext, ciphertext) authtag := ctx.computeAuthTag(data, nonce, ciphertext) - ret, out := resize(dst, len(dst)+len(ciphertext)+len(authtag)) + ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) copy(out, ciphertext) copy(out[len(ciphertext):], authtag) @@ -128,20 +128,20 @@ func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { return nil, err } - ret, out := resize(dst, len(dst)+len(plaintext)) + ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) copy(out, plaintext) return ret, nil } // Compute an authentication tag func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { - buffer := make([]byte, len(aad)+len(nonce)+len(ciphertext)+8) + buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) n := 0 n += copy(buffer, aad) n += copy(buffer[n:], nonce) n += copy(buffer[n:], ciphertext) - binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad)*8)) + binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) // According to documentation, Write() on hash.Hash never fails. hmac := hmac.New(ctx.hash, ctx.integrityKey) @@ -153,8 +153,8 @@ func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { // resize ensures the the given slice has a capacity of at least n bytes. // If the capacity of the slice is less than n, a new slice is allocated // and the existing data will be copied. -func resize(in []byte, n int) (head, tail []byte) { - if cap(in) >= n { +func resize(in []byte, n uint64) (head, tail []byte) { + if uint64(cap(in)) >= n { head = in[:n] } else { head = make([]byte, n) @@ -168,7 +168,7 @@ func resize(in []byte, n int) (head, tail []byte) { // Apply padding func padBuffer(buffer []byte, blockSize int) []byte { missing := blockSize - (len(buffer) % blockSize) - ret, out := resize(buffer, len(buffer)+missing) + ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) padding := bytes.Repeat([]byte{byte(missing)}, missing) copy(out, padding) return ret
cipher/cbc_hmac_test.go+3 −3 modified@@ -283,7 +283,7 @@ func TestTruncatedCiphertext(t *testing.T) { ct := aead.Seal(nil, nonce, data, nil) // Truncated ciphertext, but with correct auth tag - truncated, tail := resize(ct[:len(ct)-ctx.authtagBytes-2], len(ct)-2) + truncated, tail := resize(ct[:len(ct)-ctx.authtagBytes-2], uint64(len(ct))-2) copy(tail, ctx.computeAuthTag(nil, nonce, truncated[:len(truncated)-ctx.authtagBytes])) // Open should fail @@ -313,8 +313,8 @@ func TestInvalidPaddingOpen(t *testing.T) { ctx := aead.(*cbcAEAD) // Mutated ciphertext, but with correct auth tag - size := len(buffer) - ciphertext, tail := resize(buffer, size+(len(key)/2)) + size := uint64(len(buffer)) + ciphertext, tail := resize(buffer, size+(uint64(len(key))/2)) copy(tail, ctx.computeAuthTag(nil, nonce, ciphertext[:size])) // Open should fail (b/c of invalid padding, even though tag matches)
cipher/concat_kdf.go+1 −1 modified@@ -32,7 +32,7 @@ type concatKDF struct { // NewConcatKDF builds a KDF reader based on the given inputs. func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { - buffer := make([]byte, len(algID)+len(ptyUInfo)+len(ptyVInfo)+len(supPubInfo)+len(supPrivInfo)) + buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) n := 0 n += copy(buffer, algID) n += copy(buffer[n:], ptyUInfo)
cipher/ecdh_es.go+6 −1 modified@@ -24,8 +24,13 @@ import ( // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. // It is an error to call this function with a private/public key that are not on the same -// curve. Callers must ensure that the keys are valid before calling this function. +// curve. Callers must ensure that the keys are valid before calling this function. Output +// size may be at most 1<<16 bytes (64 KiB). func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { + if size > 1<<16 { + panic("ECDH-ES output size too large, must be less than 1<<16") + } + // algId, partyUInfo, partyVInfo inputs must be prefixed with the length algID := lengthPrefixed([]byte(alg)) ptyUInfo := lengthPrefixed(apuData)
c7581939a365Merge branch 'cs/164590'
5 files changed · +66 −3
asymmetric.go+4 −0 modified@@ -370,6 +370,10 @@ func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientI return nil, errors.New("square/go-jose: invalid epk header") } + if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return nil, errors.New("square/go-jose: invalid public key in epk header") + } + apuData := headers.Apu.bytes() apvData := headers.Apv.bytes()
asymmetric_test.go+30 −0 modified@@ -18,6 +18,8 @@ package jose import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "errors" @@ -429,3 +431,31 @@ func TestInvalidEllipticCurve(t *testing.T) { t.Error("should not generate ES384 signature with P-521 key") } } + +func TestInvalidECPublicKey(t *testing.T) { + // Invalid key + invalid := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: fromBase64Int("MTEx"), + Y: fromBase64Int("MTEx"), + }, + D: fromBase64Int("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo="), + } + + headers := rawHeader{ + Alg: string(ECDH_ES), + Epk: &JsonWebKey{ + Key: &invalid.PublicKey, + }, + } + + dec := ecDecrypterSigner{ + privateKey: ecTestKey256, + } + + _, err := dec.decryptKey(headers, nil, randomKeyGenerator{size: 16}) + if err == nil { + t.Fatal("decrypter accepted JWS with invalid ECDH public key") + } +}
cipher/ecdh_es.go+4 −0 modified@@ -33,6 +33,10 @@ func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, p supPubInfo := make([]byte, 4) binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) + if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { + panic("public key not on same curve as private key") + } + z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{})
cipher/ecdh_es_test.go+17 −0 modified@@ -67,6 +67,23 @@ func TestVectorECDHES(t *testing.T) { } } +func TestInvalidECPublicKey(t *testing.T) { + defer func() { recover() }() + + // Invalid key + invalid := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: fromBase64Int("MTEx"), + Y: fromBase64Int("MTEx"), + }, + D: fromBase64Int("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo="), + } + + DeriveECDHES("A128GCM", []byte{}, []byte{}, bobKey, &invalid.PublicKey, 16) + t.Fatal("should panic if public key was invalid") +} + func BenchmarkECDHES_128(b *testing.B) { apuData := []byte("APU") apvData := []byte("APV")
jwk.go+11 −3 modified@@ -23,6 +23,7 @@ import ( "crypto/rsa" "crypto/x509" "encoding/base64" + "errors" "fmt" "math/big" "reflect" @@ -277,13 +278,20 @@ func (key rawJsonWebKey) ecPublicKey() (*ecdsa.PublicKey, error) { } if key.X == nil || key.Y == nil { - return nil, fmt.Errorf("square/go-jose: invalid EC key, missing x/y values") + return nil, errors.New("square/go-jose: invalid EC key, missing x/y values") + } + + x := key.X.bigInt() + y := key.Y.bigInt() + + if !curve.IsOnCurve(x, y) { + return nil, errors.New("square/go-jose: invalid EC key, X/Y are not on declared curve") } return &ecdsa.PublicKey{ Curve: curve, - X: key.X.bigInt(), - Y: key.Y.bigInt(), + X: x, + Y: y, }, nil }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- www.openwall.com/lists/oss-security/2016/11/03/1nvdMailing ListPatchThird Party AdvisoryWEB
- github.com/square/go-jose/commit/2c5656adca9909843c4ff50acf1d2cf8f32da7e6nvdIssue TrackingPatchThird Party AdvisoryWEB
- github.com/advisories/GHSA-77gc-fj98-665hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2016-9122ghsaADVISORY
- github.com/square/go-jose/commit/789a4c4bd4c118f7564954f441b29c153ccd6a96ghsaWEB
- github.com/square/go-jose/commit/c7581939a3656bb65e89d64da0a52364a33d2507ghsaWEB
- github.com/square/go-jose/pull/111ghsaWEB
- hackerone.com/reports/169629nvdPermissions RequiredWEB
- www.openwall.com/lists/oss-security/2016/11/03/1ghsaWEB
News mentions
0No linked articles in our index yet.