VYPR
High severity7.5NVD Advisory· Published Mar 28, 2017· Updated May 13, 2026

CVE-2016-9122

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.

PackageAffected versionsPatched versions
gopkg.in/square/go-jose.v1Go
< 1.1.01.1.0

Affected products

1

Patches

3
2c5656adca99

Merge pull request #111 from square/cs/better-multi

https://github.com/square/go-joseCedric StaubSep 22, 2016via ghsa
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)
     	}
     }
     
    
789a4c4bd4c1

Use uint64 for all size calculations, size checks

https://github.com/square/go-joseCedric StaubSep 3, 2016via ghsa
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)
    
c7581939a365

Merge branch 'cs/164590'

https://github.com/square/go-joseCedric StaubAug 31, 2016via ghsa
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

News mentions

0

No linked articles in our index yet.