Skip to content

Commit 80c3e67

Browse files
author
praveenp
committed
Added support for detached jws as per 7797 for unencoded payload
1 parent be8e914 commit 80c3e67

File tree

2 files changed

+82
-11
lines changed

2 files changed

+82
-11
lines changed

jose/jws.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,68 @@ def sign(payload, key, headers=None, algorithm=ALGORITHMS.HS256):
4545
return signed_output
4646

4747

48-
def verify(token, key, algorithms, verify=True):
48+
def sign_detached(payload, key, headers=None, algorithm=ALGORITHMS.HS256):
49+
"""Signs a claims set and returns a JWS as a detached payload string, as per RFC7797
50+
51+
Args:
52+
payload (str or dict): A string to sign
53+
key (str or dict): The key to use for signing the claim set. Can be
54+
individual JWK or JWK set.
55+
headers (dict, optional): A set of headers that will be added to
56+
the default headers. Any headers that are added as additional
57+
headers will override the default headers.
58+
if the signature needs to be generated on encoded payload, then
59+
header has to contain {"b64":True}
60+
algorithm (str, optional): The algorithm to use for signing the
61+
the claims. Defaults to HS256.
62+
63+
Returns:
64+
str: The string representation of the header, and signature in detached jws format
65+
payload: the payload as received in the request or encoed if {"b4":True} header is passed in the call
66+
67+
Raises:
68+
JWSError: If there is an error signing the token.
69+
70+
Examples:
71+
72+
>>> jws.sign_detached({'a': 'b'}, 'secret', algorithm='HS256')
73+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8', {'a': 'b'}
74+
75+
76+
>>> jws.sign_detached({'a': 'b'}, 'secret', {"b64": True}, algorithm='HS256')
77+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8', eyJhIjoiYiJ9
78+
79+
"""
80+
81+
if algorithm not in ALGORITHMS.SUPPORTED:
82+
raise JWSError("Algorithm %s not supported." % algorithm)
83+
84+
if headers:
85+
if "b64" in headers and headers["b64"] is True:
86+
payload = _encode_payload(payload)
87+
headers.update({"crit": ["b64"]})
88+
else:
89+
headers = {"b64": "false"}
90+
91+
encoded_header = _encode_header(algorithm, additional_headers=headers)
92+
signed_output = _sign_header_and_claims(encoded_header, payload, algorithm, key, True)
93+
94+
return signed_output, payload
95+
96+
97+
def verify(token, key, algorithms=None, verify=True, payload=None):
4998
"""Verifies a JWS string's signature.
5099
51100
Args:
52101
token (str): A signed JWS to be verified.
53102
key (str or dict): A key to attempt to verify the payload with. Can be
54103
individual JWK or JWK set.
55104
algorithms (str or list): Valid algorithms that should be used to verify the JWS.
105+
payload (str or dict): Unencoded payload if the token is a detached jws
56106
57107
Returns:
58108
str: The str representation of the payload, assuming the signature is valid.
109+
If the token is a detached jws with "b64" true in the header, the return value will be encoded payload
59110
60111
Raises:
61112
JWSError: If there is an exception verifying a token.
@@ -65,9 +116,12 @@ def verify(token, key, algorithms, verify=True):
65116
>>> token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'
66117
>>> jws.verify(token, 'secret', algorithms='HS256')
67118
119+
>>> token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'
120+
>>> jws.verify(token, 'secret', algorithms='HS256', payload={"a":"b"})
121+
68122
"""
69123

70-
header, payload, signing_input, signature = _load(token)
124+
header, payload, signing_input, signature = _load(token, payload)
71125

72126
if verify:
73127
_verify_signature(signing_input, header, signature, key, algorithms)
@@ -126,7 +180,7 @@ def get_unverified_claims(token):
126180

127181

128182
def _encode_header(algorithm, additional_headers=None):
129-
header = {"typ": "JWT", "alg": algorithm}
183+
header = {"typ": "JOSE", "alg": algorithm}
130184

131185
if additional_headers:
132186
header.update(additional_headers)
@@ -153,7 +207,7 @@ def _encode_payload(payload):
153207
return base64url_encode(payload)
154208

155209

156-
def _sign_header_and_claims(encoded_header, encoded_claims, algorithm, key):
210+
def _sign_header_and_claims(encoded_header, encoded_claims, algorithm, key, is_detached=False):
157211
signing_input = b".".join([encoded_header, encoded_claims])
158212
try:
159213
if not isinstance(key, Key):
@@ -164,12 +218,15 @@ def _sign_header_and_claims(encoded_header, encoded_claims, algorithm, key):
164218

165219
encoded_signature = base64url_encode(signature)
166220

167-
encoded_string = b".".join([encoded_header, encoded_claims, encoded_signature])
221+
if is_detached:
222+
encoded_string = b"..".join([encoded_header, encoded_signature])
223+
else:
224+
encoded_string = b".".join([encoded_header, encoded_claims, encoded_signature])
168225

169226
return encoded_string.decode("utf-8")
170227

171228

172-
def _load(jwt):
229+
def _load(jwt, payload=None):
173230
if isinstance(jwt, str):
174231
jwt = jwt.encode("utf-8")
175232
try:
@@ -189,10 +246,15 @@ def _load(jwt):
189246
if not isinstance(header, Mapping):
190247
raise JWSError("Invalid header string: must be a json object")
191248

192-
try:
193-
payload = base64url_decode(claims_segment)
194-
except (TypeError, binascii.Error):
195-
raise JWSError("Invalid payload padding")
249+
if not payload:
250+
try:
251+
payload = base64url_decode(claims_segment)
252+
except (TypeError, binascii.Error):
253+
raise JWSError("Invalid payload padding")
254+
else:
255+
if "b64" in header and header["b64"] is True:
256+
payload = _encode_payload(payload)
257+
signing_input = b"".join([signing_input, payload])
196258

197259
try:
198260
signature = base64url_decode(crypto_segment)

tests/test_jws.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from jose.backends import RSAKey
88
from jose.constants import ALGORITHMS
99
from jose.exceptions import JWSError
10+
from jose.utils import base64url_decode, base64url_encode
1011

1112
try:
1213
from jose.backends.cryptography_backend import CryptographyRSAKey
@@ -132,7 +133,7 @@ def test_add_headers(self, payload):
132133
expected_headers = {
133134
"test": "header",
134135
"alg": "HS256",
135-
"typ": "JWT",
136+
"typ": "JOSE",
136137
}
137138

138139
token = jws.sign(payload, "secret", headers=additional_headers)
@@ -307,6 +308,14 @@ def test_jwk_set_failure(self, jwk_set):
307308
with pytest.raises(JWSError):
308309
payload = jws.verify(google_id_token, jwk_set, ALGORITHMS.RS256) # noqa: F841
309310

311+
def test_RSA256_detached(self, payload):
312+
token, payload = jws.sign_detached(payload, rsa_private_key, algorithm=ALGORITHMS.RS256)
313+
assert jws.verify(token, rsa_public_key, payload=payload) == payload
314+
315+
def test_RSA256_detached_encoded(self, payload):
316+
token, encoded_payload = jws.sign_detached(payload, rsa_private_key, {"b64": True}, algorithm=ALGORITHMS.RS256)
317+
assert jws.verify(token, rsa_public_key, payload=payload) == encoded_payload
318+
310319
def test_RSA256(self, payload):
311320
token = jws.sign(payload, rsa_private_key, algorithm=ALGORITHMS.RS256)
312321
assert jws.verify(token, rsa_public_key, ALGORITHMS.RS256) == payload

0 commit comments

Comments
 (0)