diff --git a/crypter.go b/crypter.go index 052b4e58..2cb00b68 100644 --- a/crypter.go +++ b/crypter.go @@ -28,6 +28,7 @@ import ( type Encrypter interface { Encrypt(plaintext []byte) (*JSONWebEncryption, error) EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) + Options() EncrypterOptions } // A generic content cipher @@ -57,6 +58,7 @@ type keyDecrypter interface { type genericEncrypter struct { contentAlg ContentEncryption compressionAlg CompressionAlgorithm + contentType ContentType cipher contentCipher recipients []recipientKeyInfo keyGenerator keyGenerator @@ -71,6 +73,7 @@ type recipientKeyInfo struct { // EncrypterOptions represents options that can be set on new encrypters. type EncrypterOptions struct { Compression CompressionAlgorithm + ContentType ContentType } // Recipient represents an algorithm/key to encrypt messages to. @@ -89,6 +92,7 @@ func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) } if opts != nil { encrypter.compressionAlg = opts.Compression + encrypter.contentType = opts.ContentType } if encrypter.cipher == nil { @@ -256,6 +260,7 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe obj.protected = &rawHeader{ Enc: ctx.contentAlg, + Cty: string(ctx.contentType), } obj.recipients = make([]recipientInfo, len(ctx.recipients)) @@ -312,6 +317,13 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe return obj, nil } +func (ctx *genericEncrypter) Options() EncrypterOptions { + return EncrypterOptions{ + Compression: ctx.compressionAlg, + ContentType: ctx.contentType, + } +} + // 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. diff --git a/jwt/builder.go b/jwt/builder.go index 969b5a9d..d53e9921 100644 --- a/jwt/builder.go +++ b/jwt/builder.go @@ -39,6 +39,21 @@ type Builder interface { CompactSerialize() (string, error) } +// NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens. +// Calls can be chained, and errors are accumulated until final call to +// CompactSerialize/FullSerialize. +type NestedBuilder interface { + // Claims encodes claims into JWE/JWS form. Multiple calls will merge claims + // into single JSON object. + Claims(i interface{}) NestedBuilder + // Token builds a NestedJSONWebToken from provided data. + Token() (*NestedJSONWebToken, error) + // FullSerialize serializes a token using the full serialization format. + FullSerialize() (string, error) + // CompactSerialize serializes a token using the compact serialization format. + CompactSerialize() (string, error) +} + type builder struct { payload map[string]interface{} err error @@ -54,6 +69,12 @@ type encryptedBuilder struct { enc jose.Encrypter } +type nestedBuilder struct { + builder + sig jose.Signer + enc jose.Encrypter +} + // Signed creates builder for signed tokens. func Signed(sig jose.Signer) Builder { return &signedBuilder{ @@ -68,6 +89,22 @@ func Encrypted(enc jose.Encrypter) Builder { } } +// SignedAndEncrypted creates builder for signed-then-encrypted tokens. +// ErrInvalidContentType will be returned if encrypter doesn't have JWT content type. +func SignedAndEncrypted(sig jose.Signer, enc jose.Encrypter) NestedBuilder { + if enc.Options().ContentType != "JWT" { + return &nestedBuilder{ + builder: builder{ + err: ErrInvalidContentType, + }, + } + } + return &nestedBuilder{ + sig: sig, + enc: enc, + } +} + func (b builder) claims(i interface{}) builder { if b.err != nil { return b @@ -225,3 +262,64 @@ func (b *encryptedBuilder) encrypt() (*jose.JSONWebEncryption, error) { return b.enc.Encrypt(p) } + +func (b *nestedBuilder) Claims(i interface{}) NestedBuilder { + return &nestedBuilder{ + builder: b.builder.claims(i), + sig: b.sig, + enc: b.enc, + } +} + +func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return nil, err + } + + return &NestedJSONWebToken{ + enc: enc, + Headers: []jose.Header{enc.Header}, + }, nil +} + +func (b *nestedBuilder) CompactSerialize() (string, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return "", err + } + + return enc.CompactSerialize() +} + +func (b *nestedBuilder) FullSerialize() (string, error) { + enc, err := b.signAndEncrypt() + if err != nil { + return "", err + } + + return enc.FullSerialize(), nil +} + +func (b *nestedBuilder) signAndEncrypt() (*jose.JSONWebEncryption, error) { + if b.err != nil { + return nil, b.err + } + + p, err := json.Marshal(b.payload) + if err != nil { + return nil, err + } + + sig, err := b.sig.Sign(p) + if err != nil { + return nil, err + } + + p2, err := sig.CompactSerialize() + if err != nil { + return nil, err + } + + return b.enc.Encrypt([]byte(p2)) +} diff --git a/jwt/builder_test.go b/jwt/builder_test.go index 1738e509..0a2b0d5e 100644 --- a/jwt/builder_test.go +++ b/jwt/builder_test.go @@ -179,6 +179,61 @@ func TestEncryptedFullSerializeAndToken(t *testing.T) { require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.") } +func TestBuilderSignedAndEncrypted(t *testing.T) { + recipient := jose.Recipient{ + Algorithm: jose.RSA1_5, + Key: testPrivRSAKey1.Public(), + } + encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, &jose.EncrypterOptions{ + ContentType: "JWT", + }) + require.NoError(t, err, "Error creating encrypter.") + + jwt1, err := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"}).Token() + require.NoError(t, err, "Error marshaling signed-then-encrypted token.") + if nested, err := jwt1.Decrypt(testPrivRSAKey1); assert.NoError(t, err, "Error decrypting signed-then-encrypted token.") { + out := &testClaims{} + assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out)) + assert.Equal(t, &testClaims{"foo"}, out) + } + + b := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"}) + tok1, err := b.CompactSerialize() + if assert.NoError(t, err) { + jwt, err := ParseSignedAndEncrypted(tok1) + if assert.NoError(t, err, "Error parsing signed-then-encrypted compact token.") { + if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) { + out := &testClaims{} + assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out)) + assert.Equal(t, &testClaims{"foo"}, out) + } + } + } + + tok2, err := b.FullSerialize() + if assert.NoError(t, err) { + jwt, err := ParseSignedAndEncrypted(tok2) + if assert.NoError(t, err, "Error parsing signed-then-encrypted full token.") { + if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) { + out := &testClaims{} + assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out)) + assert.Equal(t, &testClaims{"foo"}, out) + } + } + } + + b2 := SignedAndEncrypted(rsaSigner, encrypter).Claims(&invalidMarshalClaims{}) + _, err = b2.CompactSerialize() + assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.") + _, err = b2.FullSerialize() + assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.") + + encrypter2, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, nil) + require.NoError(t, err, "Error creating encrypter.") + _, err = SignedAndEncrypted(rsaSigner, encrypter2).CompactSerialize() + assert.EqualError(t, err, "square/go-jose/jwt: expected content type to be JWT (cty header)") +} + func TestBuilderHeadersSigner(t *testing.T) { tests := []struct { Keys []*rsa.PrivateKey diff --git a/jwt/errors.go b/jwt/errors.go index 5173bae4..6507dfb2 100644 --- a/jwt/errors.go +++ b/jwt/errors.go @@ -45,3 +45,6 @@ var ErrNotValidYet = errors.New("square/go-jose/jwt: validation failed, token no // ErrExpired indicates that token is used after expiry time indicated in exp claim. var ErrExpired = errors.New("square/go-jose/jwt: validation failed, token is expired (exp)") + +// ErrInvalidContentType indicated that token requires JWT cty header. +var ErrInvalidContentType = errors.New("square/go-jose/jwt: expected content type to be JWT (cty header)") diff --git a/jwt/example_test.go b/jwt/example_test.go index 9d4ee007..371146ab 100644 --- a/jwt/example_test.go +++ b/jwt/example_test.go @@ -22,6 +22,9 @@ import ( "strings" "time" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -58,7 +61,28 @@ func ExampleParseEncrypted() { panic(err) } fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject) - //Output: iss: issuer, sub: subject + // Output: iss: issuer, sub: subject +} + +func ExampleParseSignedAndEncrypted() { + raw := `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwiY3R5IjoiSldUIn0..-keV-9YpsxotBEHw.yC9SHWgnkjykgJqXZGlzYC5Wg_EdWKO5TgfqeqsWWJYw7fX9zXQE3NtXmA3nAiUrYOr3H2s0AgTeAhTNbELLEHQu0blfRaPa_uKOAgFgmhJwbGe2iFLn9J0U72wk56318nI-pTLCV8FijoGpXvAxQlaKrPLKkl9yDQimPhb7UiDwLWYkJeoayciAXhR5f40E8ORGjCz8oawXRvjDaSjgRElUwy4kMGzvJy_difemEh4lfMSIwUNVEqJkEYaalRttSymMYuV6NvBVU0N0Jb6omdM4tW961OySB4KPWCWH9UJUX0XSEcqbW9WLxpg3ftx5R7xNiCnaVaCx_gJZfXJ9yFLqztIrKh2N05zHM0tddSOwCOnq7_1rJtaVz0nTXjSjf1RrVaxJya59p3K-e41QutiGFiJGzXG-L2OyLETIaVSU3ptvaCz4IxCF3GzeCvOgaICvXkpBY1-bv-fk1ilyjmcTDnLp2KivWIxcnoQmpN9xj06ZjagdG09AHUhS5WixADAg8mIdGcanNblALecnCWG-otjM9Kw.RZoaHtSgnzOin2od3D9tnA` + tok, err := jwt.ParseSignedAndEncrypted(raw) + if err != nil { + panic(err) + } + + nested, err := tok.Decrypt(sharedEncryptionKey) + if err != nil { + panic(err) + } + + out := jwt.Claims{} + if err := nested.Claims(&rsaPrivKey.PublicKey, &out); err != nil { + panic(err) + } + + fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject) + // Output: iss: issuer, sub: subject } func ExampleClaims_Validate() { @@ -146,6 +170,32 @@ func ExampleEncrypted() { fmt.Println(raw) } +func ExampleSignedAndEncrypted() { + enc, err := jose.NewEncrypter( + jose.A128GCM, + jose.Recipient{ + Algorithm: jose.DIRECT, + Key: sharedEncryptionKey, + }, + &jose.EncrypterOptions{ + ContentType: "JWT", + }) + if err != nil { + panic(err) + } + + cl := jwt.Claims{ + Subject: "subject", + Issuer: "issuer", + } + raw, err := jwt.SignedAndEncrypted(rsaSigner, enc).Claims(cl).CompactSerialize() + if err != nil { + panic(err) + } + + fmt.Println(raw) +} + func ExampleSigned_multipleClaims() { c := &jwt.Claims{ Subject: "subject", @@ -198,3 +248,58 @@ func ExampleJSONWebToken_Claims_multiple() { fmt.Printf("iss: %s, sub: %s, scopes: %s\n", out.Issuer, out.Subject, strings.Join(out2.Scopes, ",")) // Output: iss: issuer, sub: subject, scopes: foo,bar } + +func mustUnmarshalRSA(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil { + panic("failed to decode PEM data") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + panic("failed to parse RSA key: " + err.Error()) + } + if key, ok := key.(*rsa.PrivateKey); ok { + return key + } + panic("key is not of type *rsa.PrivateKey") +} + +func mustMakeSigner(alg jose.SignatureAlgorithm, k interface{}) jose.Signer { + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: k}, nil) + if err != nil { + panic("failed to create signer:" + err.Error()) + } + + return sig +} + +var rsaPrivKey = mustUnmarshalRSA(`-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIHBvDHAr7jh8h +xaqBCl11fjI9YZtdC5b3HtXTXZW3c2dIOImNUjffT8POP6p5OpzivmC1om7iOyuZ +3nJjC9LT3zqqs3f2i5d4mImxEuqG6uWdryFfkp0uIv5VkjVO+iQWd6pDAPGP7r1Z +foXCleyCtmyNH4JSkJneNPOk/4BxO8vcvRnCMT/Gv81IT6H+OQ6OovWOuJr8RX9t +1wuCjC9ezZxeI9ONffhiO5FMrVh5H9LJTl3dPOVa4aEcOvgd45hBmvxAyXqf8daE +6Kl2O7vQ4uwgnSTVXYIIjCjbepuersApIMGx/XPSgiU1K3Xtah/TBvep+S3VlwPc +q/QH25S9AgMBAAECggEAe+y8XKYfPw4SxY1uPB+5JSwT3ON3nbWxtjSIYy9Pqp5z +Vcx9kuFZ7JevQSk4X38m7VzM8282kC/ono+d8yy9Uayq3k/qeOqV0X9Vti1qxEbw +ECkG1/MqGApfy4qSLOjINInDDV+mOWa2KJgsKgdCwuhKbVMYGB2ozG2qfYIlfvlY +vLcBEpGWmswJHNmkcjTtGFIyJgPbsI6ndkkOeQbqQKAaadXtG1xUzH+vIvqaUl/l +AkNf+p4qhPkHsoAWXf1qu9cYa2T8T+mEo79AwlgVC6awXQWNRTiyClDJC7cu6NBy +ZHXCLFMbalzWF9qeI2OPaFX2x3IBWrbyDxcJ4TSdQQKBgQD/Fp/uQonMBh1h4Vi4 +HlxZdqSOArTitXValdLFGVJ23MngTGV/St4WH6eRp4ICfPyldsfcv6MZpNwNm1Rn +lB5Gtpqpby1dsrOSfvVbY7U3vpLnd8+hJ/lT5zCYt5Eor46N6iWRkYWzNe4PixiF +z1puGUvFCbZdeeACVrPLmW3JKQKBgQDI0y9WTf8ezKPbtap4UEE6yBf49ftohVGz +p4iD6Ng1uqePwKahwoVXKOc179CjGGtW/UUBORAoKRmxdHajHq6LJgsBxpaARz21 +COPy99BUyp9ER5P8vYn63lC7Cpd/K7uyMjaz1DAzYBZIeVZHIw8O9wuGNJKjRFy9 +SZyD3V0ddQKBgFMdohrWH2QVEfnUnT3Q1rJn0BJdm2bLTWOosbZ7G72TD0xAWEnz +sQ1wXv88n0YER6X6YADziEdQykq8s/HT91F/KkHO8e83zP8M0xFmGaQCOoelKEgQ +aFMIX3NDTM7+9OoUwwz9Z50PE3SJFAJ1n7eEEoYvNfabQXxBl+/dHEKRAoGAPEvU +EaiXacrtg8EWrssB2sFLGU/ZrTciIbuybFCT4gXp22pvXXAHEvVP/kzDqsRhLhwb +BNP6OuSkNziNikpjA5pngZ/7fgZly54gusmW/m5bxWdsUl0iOXVYbeAvPlqGH2me +LP4Pfs1hw17S/cbT9Z1NE31jbavP4HFikeD73SUCgYEArQfuudml6ei7XZ1Emjq8 +jZiD+fX6e6BD/ISatVnuyZmGj9wPFsEhY2BpLiAMQHMDIvH9nlKzsFvjkTPB86qG +jCh3D67Os8eSBk5uRC6iW3Fc4DXvB5EFS0W9/15Sl+V5vXAcrNMpYS82OTSMG2Gt +b9Ym/nxaqyTu0PxajXkKm5Q= +-----END PRIVATE KEY-----`) + +var rsaSigner = mustMakeSigner(jose.RS256, rsaPrivKey) diff --git a/jwt/jwt.go b/jwt/jwt.go index 73adfcc2..9efa6122 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -20,6 +20,7 @@ package jwt import ( "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/json" + "strings" ) // JSONWebToken represents a JSON Web Token (as specified in RFC7519). @@ -28,6 +29,11 @@ type JSONWebToken struct { Headers []jose.Header } +type NestedJSONWebToken struct { + enc *jose.JSONWebEncryption + Headers []jose.Header +} + // Claims deserializes a JSONWebToken into dest using the provided key. func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) error { b, err := t.payload(key) @@ -44,6 +50,20 @@ func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) error { return nil } +func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken, error) { + b, err := t.enc.Decrypt(decryptionKey) + if err != nil { + return nil, err + } + + sig, err := ParseSigned(string(b)) + if err != nil { + return nil, err + } + + return sig, nil +} + // ParseSigned parses token from JWS form. func ParseSigned(s string) (*JSONWebToken, error) { sig, err := jose.ParseSigned(s) @@ -55,7 +75,10 @@ func ParseSigned(s string) (*JSONWebToken, error) { headers[i] = signature.Header } - return &JSONWebToken{sig.Verify, headers}, nil + return &JSONWebToken{ + payload: sig.Verify, + Headers: headers, + }, nil } // ParseEncrypted parses token from JWE form. @@ -65,5 +88,25 @@ func ParseEncrypted(s string) (*JSONWebToken, error) { return nil, err } - return &JSONWebToken{enc.Decrypt, []jose.Header{enc.Header}}, nil + return &JSONWebToken{ + payload: enc.Decrypt, + Headers: []jose.Header{enc.Header}, + }, nil +} + +// ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form. +func ParseSignedAndEncrypted(s string) (*NestedJSONWebToken, error) { + enc, err := jose.ParseEncrypted(s) + if err != nil { + return nil, err + } + + if strings.ToUpper(enc.Header.ContentType) != "JWT" { + return nil, ErrInvalidContentType + } + + return &NestedJSONWebToken{ + enc: enc, + Headers: []jose.Header{enc.Header}, + }, nil } diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index d322da3a..130c283f 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -24,14 +24,16 @@ import ( ) var ( - hmacSignedToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwic2NvcGVzIjpbInMxIiwiczIiXX0.Y6_PfQHrzRJ_Vlxij5VI07-pgDIuJNN3Z_g5sSaGQ0c` - rsaSignedToken = `eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzY29wZXMiOlsiczEiLCJzMiJdLCJzdWIiOiJzdWJqZWN0In0.UDDtyK9gC9kyHltcP7E_XODsnqcJWZIiXeGmSAH7SE9YKy3N0KSfFIN85dCNjTfs6zvy4rkrCHzLB7uKAtzMearh3q7jL4nxbhUMhlUcs_9QDVoN4q_j58XmRqBqRnBk-RmDu9TgcV8RbErP4awpIhwWb5UU-hR__4_iNbHdKqwSUPDKYGlf5eicuiYrPxH8mxivk4LRD-vyRdBZZKBt0XIDnEU4TdcNCzAXojkftqcFWYsczwS8R4JHd1qYsMyiaWl4trdHZkO4QkeLe34z4ZAaPMt3wE-gcU-VoqYTGxz-K3Le2VaZ0r3j_z6bOInsv0yngC_cD1dCXMyQJWnWjQ` - invalidPayloadSignedToken = `eyJhbGciOiJIUzI1NiJ9.aW52YWxpZC1wYXlsb2Fk.ScBKKm18jcaMLGYDNRUqB5gVMRZl4DM6dh3ShcxeNgY` - invalidPartsSignedToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwic2NvcGVzIjpbInMxIiwiczIiXX0` - hmacEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..NZrU98U4QNO0y-u6.HSq5CvlmkUT1BPqLGZ4.1-zuiZ4RbHrTTUoA8Dvfhg` - rsaEncryptedToken = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.IvkVHHiI8JwwavvTR80xGjYvkzubMrZ-TDDx8k8SNJMEylfFfNUc7F2rC3WAABF_xmJ3SW2A6on-S6EAG97k0RsjqHHNqZuaFpDvjeuLqZFfYKzI45aCtkGG4C2ij2GbeySqJ784CcvFJPUWJ-6VPN2Ho2nhefUSqig0jE2IvOKy1ywTj_VBVBxF_dyXFnXwxPKGUQr3apxrWeRJfDh2Cf8YPBlLiRznjfBfwgePB1jP7WCZNwItj10L7hsT_YWEx01XJcbxHaXFLwKyVzwWaDhreFyaWMRbGqEfqVuOT34zfmhLDhQlgLLwkXrvYqX90NsQ9Ftg0LLIfRMbsfdgug.BFy2Tj1RZN8yq2Lk-kMiZQ.9Z0eOyPiv5cEzmXh64RlAQ36Uvz0WpZgqRcc2_69zHTmUOv0Vnl1I6ks8sTraUEvukAilolNBjBj47s0b4b-Og.VM8-eJg5ZsqnTqs0LtGX_Q` - invalidPayloadEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..T4jCS4Yyw1GCH0aW.y4gFaMITdBs_QZM8RKrL.6MPyk1cMVaOJFoNGlEuaRQ` - invalidPartsEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..NZrU98U4QNO0y-u6.HSq5CvlmkUT1BPqLGZ4` + hmacSignedToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwic2NvcGVzIjpbInMxIiwiczIiXX0.Y6_PfQHrzRJ_Vlxij5VI07-pgDIuJNN3Z_g5sSaGQ0c` + rsaSignedToken = `eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzY29wZXMiOlsiczEiLCJzMiJdLCJzdWIiOiJzdWJqZWN0In0.UDDtyK9gC9kyHltcP7E_XODsnqcJWZIiXeGmSAH7SE9YKy3N0KSfFIN85dCNjTfs6zvy4rkrCHzLB7uKAtzMearh3q7jL4nxbhUMhlUcs_9QDVoN4q_j58XmRqBqRnBk-RmDu9TgcV8RbErP4awpIhwWb5UU-hR__4_iNbHdKqwSUPDKYGlf5eicuiYrPxH8mxivk4LRD-vyRdBZZKBt0XIDnEU4TdcNCzAXojkftqcFWYsczwS8R4JHd1qYsMyiaWl4trdHZkO4QkeLe34z4ZAaPMt3wE-gcU-VoqYTGxz-K3Le2VaZ0r3j_z6bOInsv0yngC_cD1dCXMyQJWnWjQ` + invalidPayloadSignedToken = `eyJhbGciOiJIUzI1NiJ9.aW52YWxpZC1wYXlsb2Fk.ScBKKm18jcaMLGYDNRUqB5gVMRZl4DM6dh3ShcxeNgY` + invalidPartsSignedToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwic2NvcGVzIjpbInMxIiwiczIiXX0` + hmacEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..NZrU98U4QNO0y-u6.HSq5CvlmkUT1BPqLGZ4.1-zuiZ4RbHrTTUoA8Dvfhg` + rsaEncryptedToken = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.IvkVHHiI8JwwavvTR80xGjYvkzubMrZ-TDDx8k8SNJMEylfFfNUc7F2rC3WAABF_xmJ3SW2A6on-S6EAG97k0RsjqHHNqZuaFpDvjeuLqZFfYKzI45aCtkGG4C2ij2GbeySqJ784CcvFJPUWJ-6VPN2Ho2nhefUSqig0jE2IvOKy1ywTj_VBVBxF_dyXFnXwxPKGUQr3apxrWeRJfDh2Cf8YPBlLiRznjfBfwgePB1jP7WCZNwItj10L7hsT_YWEx01XJcbxHaXFLwKyVzwWaDhreFyaWMRbGqEfqVuOT34zfmhLDhQlgLLwkXrvYqX90NsQ9Ftg0LLIfRMbsfdgug.BFy2Tj1RZN8yq2Lk-kMiZQ.9Z0eOyPiv5cEzmXh64RlAQ36Uvz0WpZgqRcc2_69zHTmUOv0Vnl1I6ks8sTraUEvukAilolNBjBj47s0b4b-Og.VM8-eJg5ZsqnTqs0LtGX_Q` + invalidPayloadEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..T4jCS4Yyw1GCH0aW.y4gFaMITdBs_QZM8RKrL.6MPyk1cMVaOJFoNGlEuaRQ` + invalidPartsEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..NZrU98U4QNO0y-u6.HSq5CvlmkUT1BPqLGZ4` + signedAndEncryptedToken = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldUIn0.icnR7M1HSgMDaUnJhfzT5nLmT0eRPeNsKPkioNcyq9TZsm-LgbE7wZkNFGfQqYwvbmrZ3UpOhNkrq4n2KN3N1dtjH9TVxzfMxz2OMh0dRWUNMi58EMadhmIpH3PLyyaeDyd0dyHpOIRPFTAoOdn2GoO_flV5CvPMhgdVKYB3h3vQW-ZZDu4cOZwXAjTuThdoUZCNWFhJhXyj-PrKLyVpX6rE1o4X05IS8008SLZyx-PZlsUPyLs6CJi7Z4PzZRzOJTV00a-7UOi-fBKBZV5V8eRpWuzJ673pMALlRCBzrRin-JeEA_QnAejtMAHG7RSGP60easQN4I-0jLTQNNNynw.oFrO-5ZgRrnWmbkPsbyMiQ.BVaWUzlrdfhe0otPJpb3DGoDCT6-BOmN_Pgq5NOqVFYIAwG5pM4pf7TaiPUJeQLf0phbLgpT4RfJ20Zhwfc2MH5unCqc8TZEP2dOrYRhb8o-X57x6IQppIDbjK2i_CAWf3yF5JUB7qRqOizpKZTh3HFTVEglY3WF8tAJ8KpnatTUmwcnqlyjdBFvYu4usiyvc_u9wNbXx5-lFt0slQYleHQMUirBprKyswIBjMoFJEe7kDvU_MCKI4NI9_fSfWJpaUdNxQEvRYR1PV4ZQdwBY0X9u2n2QH5iVQMrmgmQ5hPbWxwRv1-7jXBMPBpGeFQZHeEtSwif1_Umwyt8cDyRChb3OM7XQ3eY0UJRrbmvhcLWIcMp8FpblDaBinbjD6qIVXZVmaAdIbi2a_HblfoeL3-UABb82AAxOqQcAFjDEDTR2TFalDXSwgPZrAaQ_Mql3eFe9r2y0UVkgG7XYF4ik8sSK48CkZPUvkZFr-K9QMq-RZLzT3Zw0edxNaKgje27S26H9qClh6CCr9nk38AZZ76_Xz7f-Fil5xI0Dq95UzvwW__U3JJWE6OVUVx_RVJgdOJn8_B7hluckwBLUblscA.83pPXNnH0sKgHvFboiJVDA` + invalidSignedAndEncryptedToken = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.QKYu3DkFEXBUa2U0Sgtm-e44BMuaFVbMu2T-GB3qEGONrmOuaB5BtNCvBUnuj6HR0v6u-tvawToRSzExQQLFTvPcLiQR8iclWirqAFUqLrg8kRU3qIRLkmErYeGIfCML1jq9ofKg0DI5-YrU5RSyUg9cwfXKEx8KNwFcjeVeDZwWEACdU8xBnQp57rNfr0Tj-dPnGKID7LU5ZV0vhK90FpEG7UqOeSHFmvONQyz6Ca-ZkE8X2swqGad-q5xl8f9pApdFqHzADox5OlgtxPkr-Khkm6WGfvf1K_e-iW5LYtvWIAjNByft2TexsNcYpdAO2oNAgh2nkhoohl-zCWU-og.UAU65JWKqvHZ_Z0V-xLyjQ.M6sQ4lAzKFelSmL6C6uoK00rB8IFCAK-eJ0iByGhtg8eYtmSBFsP_oUySfKPtxcPRkQ7YxnEX5D-DOo20wCV7il2Be9No__0R6_5heISOMXcKmKP3D6pFusaPisNGOgLw8SKXBuVpe20PvOJ9RgOXRKucSR2UMINXtqIn9RdxbKOlBBmMJhnX4TeQ00fRILng2sMbUHsWExSthQODHGx6VcwLFp-Aqmsnv2q2KkLpA8sEm48AHHFQXSGtlVGVgWKi3dOQYUnDJW4P64Xxr1Uq3yT7w_dRwK4BA7l3Biecj5dwkKrFMJ_RaCt-ED_R15zpxg6PmnXeeJnif58Fai40ZWOsGvLZNYwL1jbi-TrsargpdUQedfzuTk8Na2NkCzFNg2BYXVDHJ_WAX1daVyhvunaURwAlBatAcmnOGxWebwV1xQoQ7iHg6ZGohCannn_pqGwJlMHMgnCcnCIhwfj9uL9Ejz_TVceZNMlT1KvLRafVfxGhkp48bdnd8OcXmjT9pQzZUB3OqrstWKhbItZ1xMpy6dZ54ldWvtTTyQ4tQJaVWgXERUM1erDT6Ypyl15-fumOB9MRcgMG3NDblKowA.P9WTBITvVUgrLjX6bS0opQ` ) type customClaims struct { @@ -101,6 +103,25 @@ func TestDecodeToken(t *testing.T) { _, err = ParseEncrypted(invalidPartsEncryptedToken) assert.EqualError(t, err, "square/go-jose: compact JWE format must have five parts") + + tok7, err := ParseSignedAndEncrypted(signedAndEncryptedToken) + if assert.NoError(t, err, "Error parsing signed-then-encrypted token.") { + c := make(map[string]interface{}) + if nested, err := tok7.Decrypt(testPrivRSAKey1); assert.NoError(t, err) { + assert.NoError(t, nested.Claims(testPrivRSAKey1.Public(), &c)) + assert.Equal(t, map[string]interface{}{ + "sub": "subject", + "iss": "issuer", + "scopes": []interface{}{"s1", "s2"}, + }, c) + assert.EqualError(t, nested.Claims(testPrivRSAKey2.Public()), "square/go-jose: error in cryptographic primitive") + } + } + _, err = tok7.Decrypt(testPrivRSAKey2) + assert.EqualError(t, err, "square/go-jose: error in cryptographic primitive") + + _, err = ParseSignedAndEncrypted(invalidSignedAndEncryptedToken) + assert.EqualError(t, err, "square/go-jose/jwt: expected content type to be JWT (cty header)") } func BenchmarkDecodeSignedToken(b *testing.B) { diff --git a/shared.go b/shared.go index 17373f00..dfafc017 100644 --- a/shared.go +++ b/shared.go @@ -34,6 +34,9 @@ type ContentEncryption string // CompressionAlgorithm represents an algorithm used for plaintext compression. type CompressionAlgorithm string +// ContentType represents type of the contained data. +type ContentType string + var ( // ErrCryptoFailure represents an error in cryptographic primitive. This // occurs when, for example, a message had an invalid authentication tag or @@ -128,23 +131,26 @@ type rawHeader struct { Jwk *JSONWebKey `json:"jwk,omitempty"` Kid string `json:"kid,omitempty"` Nonce string `json:"nonce,omitempty"` + Cty string `json:"cty,omitempty"` } // Header represents the read-only JOSE header for JWE/JWS objects. type Header struct { - KeyID string - JSONWebKey *JSONWebKey - Algorithm string - Nonce string + KeyID string + JSONWebKey *JSONWebKey + Algorithm string + Nonce string + ContentType string } // sanitized produces a cleaned-up header object from the raw JSON. func (parsed rawHeader) sanitized() Header { return Header{ - KeyID: parsed.Kid, - JSONWebKey: parsed.Jwk, - Algorithm: parsed.Alg, - Nonce: parsed.Nonce, + KeyID: parsed.Kid, + JSONWebKey: parsed.Jwk, + Algorithm: parsed.Alg, + Nonce: parsed.Nonce, + ContentType: parsed.Cty, } } @@ -193,6 +199,9 @@ func (dst *rawHeader) merge(src *rawHeader) { if dst.Nonce == "" { dst.Nonce = src.Nonce } + if dst.Cty == "" { + dst.Cty = src.Cty + } } // Get JOSE name of curve