mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 03:51:08 -05:00
first commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
206
venv/lib/python3.11/site-packages/jwcrypto/common.py
Normal file
206
venv/lib/python3.11/site-packages/jwcrypto/common.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
|
||||
|
||||
import copy
|
||||
import json
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
from collections import namedtuple
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
# Padding stripping versions as described in
|
||||
# RFC 7515 Appendix C
|
||||
|
||||
|
||||
def base64url_encode(payload):
|
||||
if not isinstance(payload, bytes):
|
||||
payload = payload.encode('utf-8')
|
||||
encode = urlsafe_b64encode(payload)
|
||||
return encode.decode('utf-8').rstrip('=')
|
||||
|
||||
|
||||
def base64url_decode(payload):
|
||||
size = len(payload) % 4
|
||||
if size == 2:
|
||||
payload += '=='
|
||||
elif size == 3:
|
||||
payload += '='
|
||||
elif size != 0:
|
||||
raise ValueError('Invalid base64 string')
|
||||
return urlsafe_b64decode(payload.encode('utf-8'))
|
||||
|
||||
|
||||
# JSON encoding/decoding helpers with good defaults
|
||||
|
||||
def json_encode(string):
|
||||
if isinstance(string, bytes):
|
||||
string = string.decode('utf-8')
|
||||
return json.dumps(string, separators=(',', ':'), sort_keys=True)
|
||||
|
||||
|
||||
def json_decode(string):
|
||||
if isinstance(string, bytes):
|
||||
string = string.decode('utf-8')
|
||||
return json.loads(string)
|
||||
|
||||
|
||||
class JWException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidJWAAlgorithm(JWException):
|
||||
def __init__(self, message=None):
|
||||
msg = 'Invalid JWA Algorithm name'
|
||||
if message:
|
||||
msg += ' (%s)' % message
|
||||
super(InvalidJWAAlgorithm, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidCEKeyLength(JWException):
|
||||
"""Invalid CEK Key Length.
|
||||
|
||||
This exception is raised when a Content Encryption Key does not match
|
||||
the required length.
|
||||
"""
|
||||
|
||||
def __init__(self, expected, obtained):
|
||||
msg = 'Expected key of length %d bits, got %d' % (expected, obtained)
|
||||
super(InvalidCEKeyLength, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWEOperation(JWException):
|
||||
"""Invalid JWS Object.
|
||||
|
||||
This exception is raised when a requested operation cannot
|
||||
be execute due to unsatisfied conditions.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = message
|
||||
else:
|
||||
msg = 'Unknown Operation Failure'
|
||||
if exception:
|
||||
msg += ' {%s}' % repr(exception)
|
||||
super(InvalidJWEOperation, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWEKeyType(JWException):
|
||||
"""Invalid JWE Key Type.
|
||||
|
||||
This exception is raised when the provided JWK Key does not match
|
||||
the type required by the specified algorithm.
|
||||
"""
|
||||
|
||||
def __init__(self, expected, obtained):
|
||||
msg = 'Expected key type %s, got %s' % (expected, obtained)
|
||||
super(InvalidJWEKeyType, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWEKeyLength(JWException):
|
||||
"""Invalid JWE Key Length.
|
||||
|
||||
This exception is raised when the provided JWK Key does not match
|
||||
the length required by the specified algorithm.
|
||||
"""
|
||||
|
||||
def __init__(self, expected, obtained):
|
||||
msg = 'Expected key of length %d, got %d' % (expected, obtained)
|
||||
super(InvalidJWEKeyLength, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWSERegOperation(JWException):
|
||||
"""Invalid JWSE Header Registry Operation.
|
||||
|
||||
This exception is raised when there is an error in trying to add a JW
|
||||
Signature or Encryption header to the Registry.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = message
|
||||
else:
|
||||
msg = 'Unknown Operation Failure'
|
||||
if exception:
|
||||
msg += ' {%s}' % repr(exception)
|
||||
super(InvalidJWSERegOperation, self).__init__(msg)
|
||||
|
||||
|
||||
class JWKeyNotFound(JWException):
|
||||
"""The key needed to complete the operation was not found.
|
||||
|
||||
This exception is raised when a JWKSet is used to perform
|
||||
some operation and the key required to successfully complete
|
||||
the operation is not found.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None):
|
||||
if message:
|
||||
msg = message
|
||||
else:
|
||||
msg = 'Key Not Found'
|
||||
super(JWKeyNotFound, self).__init__(msg)
|
||||
|
||||
|
||||
# JWSE Header Registry definitions
|
||||
|
||||
# RFC 7515 - 9.1: JSON Web Signature and Encryption Header Parameters Registry
|
||||
# HeaderParameters are for both JWS and JWE
|
||||
JWSEHeaderParameter = namedtuple('Parameter',
|
||||
'description mustprotect supported check_fn')
|
||||
|
||||
|
||||
class JWSEHeaderRegistry(MutableMapping):
|
||||
def __init__(self, init_registry=None):
|
||||
if init_registry:
|
||||
if isinstance(init_registry, dict):
|
||||
self._registry = copy.deepcopy(init_registry)
|
||||
else:
|
||||
raise InvalidJWSERegOperation('Unknown input type')
|
||||
else:
|
||||
self._registry = {}
|
||||
|
||||
MutableMapping.__init__(self)
|
||||
|
||||
def check_header(self, h, value):
|
||||
if h not in self._registry:
|
||||
raise InvalidJWSERegOperation('No header "%s" found in registry'
|
||||
% h)
|
||||
|
||||
param = self._registry[h]
|
||||
if param.check_fn is None:
|
||||
return True
|
||||
else:
|
||||
return param.check_fn(value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._registry.__getitem__(key)
|
||||
|
||||
def __iter__(self):
|
||||
return self._registry.__iter__()
|
||||
|
||||
def __delitem__(self, key):
|
||||
if self._registry[key].mustprotect or \
|
||||
self._registry[key].supported:
|
||||
raise InvalidJWSERegOperation('Unable to delete protected or '
|
||||
'supported field')
|
||||
else:
|
||||
self._registry.__delitem__(key)
|
||||
|
||||
def __setitem__(self, h, jwse_header_param):
|
||||
# Check if a header is not supported
|
||||
if h in self._registry:
|
||||
p = self._registry[h]
|
||||
if p.supported:
|
||||
raise InvalidJWSERegOperation('Supported header already exists'
|
||||
' in registry')
|
||||
elif p.mustprotect and not jwse_header_param.mustprotect:
|
||||
raise InvalidJWSERegOperation('Header specified should be'
|
||||
'a protected header')
|
||||
else:
|
||||
del self._registry[h]
|
||||
|
||||
self._registry[h] = jwse_header_param
|
||||
|
||||
def __len__(self):
|
||||
return self._registry.__len__()
|
||||
1202
venv/lib/python3.11/site-packages/jwcrypto/jwa.py
Normal file
1202
venv/lib/python3.11/site-packages/jwcrypto/jwa.py
Normal file
File diff suppressed because it is too large
Load Diff
615
venv/lib/python3.11/site-packages/jwcrypto/jwe.py
Normal file
615
venv/lib/python3.11/site-packages/jwcrypto/jwe.py
Normal file
@@ -0,0 +1,615 @@
|
||||
# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
|
||||
|
||||
import zlib
|
||||
|
||||
from jwcrypto import common
|
||||
from jwcrypto.common import JWException, JWKeyNotFound
|
||||
from jwcrypto.common import JWSEHeaderParameter, JWSEHeaderRegistry
|
||||
from jwcrypto.common import base64url_decode, base64url_encode
|
||||
from jwcrypto.common import json_decode, json_encode
|
||||
from jwcrypto.jwa import JWA
|
||||
from jwcrypto.jwk import JWKSet
|
||||
|
||||
# Limit the amount of data we are willing to decompress by default.
|
||||
default_max_compressed_size = 256 * 1024
|
||||
|
||||
|
||||
# RFC 7516 - 4.1
|
||||
# name: (description, supported?)
|
||||
JWEHeaderRegistry = {
|
||||
'alg': JWSEHeaderParameter('Algorithm', False, True, None),
|
||||
'enc': JWSEHeaderParameter('Encryption Algorithm', False, True, None),
|
||||
'zip': JWSEHeaderParameter('Compression Algorithm', False, True, None),
|
||||
'jku': JWSEHeaderParameter('JWK Set URL', False, False, None),
|
||||
'jwk': JWSEHeaderParameter('JSON Web Key', False, False, None),
|
||||
'kid': JWSEHeaderParameter('Key ID', False, True, None),
|
||||
'x5u': JWSEHeaderParameter('X.509 URL', False, False, None),
|
||||
'x5c': JWSEHeaderParameter('X.509 Certificate Chain', False, False, None),
|
||||
'x5t': JWSEHeaderParameter('X.509 Certificate SHA-1 Thumbprint', False,
|
||||
False, None),
|
||||
'x5t#S256': JWSEHeaderParameter('X.509 Certificate SHA-256 Thumbprint',
|
||||
False, False, None),
|
||||
'typ': JWSEHeaderParameter('Type', False, True, None),
|
||||
'cty': JWSEHeaderParameter('Content Type', False, True, None),
|
||||
'crit': JWSEHeaderParameter('Critical', True, True, None),
|
||||
}
|
||||
"""Registry of valid header parameters"""
|
||||
|
||||
default_allowed_algs = [
|
||||
# Key Management Algorithms
|
||||
'RSA-OAEP', 'RSA-OAEP-256',
|
||||
'A128KW', 'A192KW', 'A256KW',
|
||||
'dir',
|
||||
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
|
||||
'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
|
||||
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
|
||||
# Content Encryption Algorithms
|
||||
'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512',
|
||||
'A128GCM', 'A192GCM', 'A256GCM']
|
||||
"""Default allowed algorithms"""
|
||||
|
||||
|
||||
class InvalidJWEData(JWException):
|
||||
"""Invalid JWE Object.
|
||||
|
||||
This exception is raised when the JWE Object is invalid and/or
|
||||
improperly formatted.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = message
|
||||
else:
|
||||
msg = 'Unknown Data Verification Failure'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(InvalidJWEData, self).__init__(msg)
|
||||
|
||||
|
||||
# These have been moved to jwcrypto.common, maintain here for backwards compat
|
||||
InvalidCEKeyLength = common.InvalidCEKeyLength
|
||||
InvalidJWEKeyLength = common.InvalidJWEKeyLength
|
||||
InvalidJWEKeyType = common.InvalidJWEKeyType
|
||||
InvalidJWEOperation = common.InvalidJWEOperation
|
||||
|
||||
|
||||
class JWE:
|
||||
"""JSON Web Encryption object
|
||||
|
||||
This object represent a JWE token.
|
||||
"""
|
||||
|
||||
def __init__(self, plaintext=None, protected=None, unprotected=None,
|
||||
aad=None, algs=None, recipient=None, header=None,
|
||||
header_registry=None):
|
||||
"""Creates a JWE token.
|
||||
|
||||
:param plaintext(bytes): An arbitrary plaintext to be encrypted.
|
||||
:param protected: A JSON string with the protected header.
|
||||
:param unprotected: A JSON string with the shared unprotected header.
|
||||
:param aad(bytes): Arbitrary additional authenticated data
|
||||
:param algs: An optional list of allowed algorithms
|
||||
:param recipient: An optional, default recipient key
|
||||
:param header: An optional header for the default recipient
|
||||
:param header_registry: Optional additions to the header registry
|
||||
"""
|
||||
self._allowed_algs = None
|
||||
self.objects = {}
|
||||
self.plaintext = None
|
||||
self.header_registry = JWSEHeaderRegistry(JWEHeaderRegistry)
|
||||
if header_registry:
|
||||
self.header_registry.update(header_registry)
|
||||
if plaintext is not None:
|
||||
if isinstance(plaintext, bytes):
|
||||
self.plaintext = plaintext
|
||||
else:
|
||||
self.plaintext = plaintext.encode('utf-8')
|
||||
self.cek = None
|
||||
self.decryptlog = None
|
||||
if aad:
|
||||
self.objects['aad'] = aad
|
||||
if protected:
|
||||
if isinstance(protected, dict):
|
||||
protected = json_encode(protected)
|
||||
else:
|
||||
json_decode(protected) # check header encoding
|
||||
self.objects['protected'] = protected
|
||||
if unprotected:
|
||||
if isinstance(unprotected, dict):
|
||||
unprotected = json_encode(unprotected)
|
||||
else:
|
||||
json_decode(unprotected) # check header encoding
|
||||
self.objects['unprotected'] = unprotected
|
||||
if algs:
|
||||
self._allowed_algs = algs
|
||||
|
||||
if recipient:
|
||||
self.add_recipient(recipient, header=header)
|
||||
elif header:
|
||||
raise ValueError('Header is allowed only with default recipient')
|
||||
|
||||
def _jwa_keymgmt(self, name):
|
||||
allowed = self._allowed_algs or default_allowed_algs
|
||||
if name not in allowed:
|
||||
raise InvalidJWEOperation('Algorithm not allowed')
|
||||
return JWA.keymgmt_alg(name)
|
||||
|
||||
def _jwa_enc(self, name):
|
||||
allowed = self._allowed_algs or default_allowed_algs
|
||||
if name not in allowed:
|
||||
raise InvalidJWEOperation('Algorithm not allowed')
|
||||
return JWA.encryption_alg(name)
|
||||
|
||||
@property
|
||||
def allowed_algs(self):
|
||||
"""Allowed algorithms.
|
||||
|
||||
The list of allowed algorithms.
|
||||
Can be changed by setting a list of algorithm names.
|
||||
"""
|
||||
|
||||
if self._allowed_algs:
|
||||
return self._allowed_algs
|
||||
else:
|
||||
return default_allowed_algs
|
||||
|
||||
@allowed_algs.setter
|
||||
def allowed_algs(self, algs):
|
||||
if not isinstance(algs, list):
|
||||
raise TypeError('Allowed Algs must be a list')
|
||||
self._allowed_algs = algs
|
||||
|
||||
def _merge_headers(self, h1, h2):
|
||||
for k in list(h1.keys()):
|
||||
if k in h2:
|
||||
raise InvalidJWEData('Duplicate header: "%s"' % k)
|
||||
h1.update(h2)
|
||||
return h1
|
||||
|
||||
def _get_jose_header(self, header=None):
|
||||
jh = {}
|
||||
if 'protected' in self.objects:
|
||||
ph = json_decode(self.objects['protected'])
|
||||
jh = self._merge_headers(jh, ph)
|
||||
if 'unprotected' in self.objects:
|
||||
uh = json_decode(self.objects['unprotected'])
|
||||
jh = self._merge_headers(jh, uh)
|
||||
if header:
|
||||
rh = json_decode(header)
|
||||
jh = self._merge_headers(jh, rh)
|
||||
return jh
|
||||
|
||||
def _get_alg_enc_from_headers(self, jh):
|
||||
algname = jh.get('alg', None)
|
||||
if algname is None:
|
||||
raise InvalidJWEData('Missing "alg" from headers')
|
||||
alg = self._jwa_keymgmt(algname)
|
||||
encname = jh.get('enc', None)
|
||||
if encname is None:
|
||||
raise InvalidJWEData('Missing "enc" from headers')
|
||||
enc = self._jwa_enc(encname)
|
||||
return alg, enc
|
||||
|
||||
def _encrypt(self, alg, enc, jh):
|
||||
aad = base64url_encode(self.objects.get('protected', ''))
|
||||
if 'aad' in self.objects:
|
||||
aad += '.' + base64url_encode(self.objects['aad'])
|
||||
aad = aad.encode('utf-8')
|
||||
|
||||
compress = jh.get('zip', None)
|
||||
if compress == 'DEF':
|
||||
data = zlib.compress(self.plaintext)[2:-4]
|
||||
elif compress is None:
|
||||
data = self.plaintext
|
||||
else:
|
||||
raise ValueError('Unknown compression')
|
||||
|
||||
iv, ciphertext, tag = enc.encrypt(self.cek, aad, data)
|
||||
self.objects['iv'] = iv
|
||||
self.objects['ciphertext'] = ciphertext
|
||||
self.objects['tag'] = tag
|
||||
|
||||
def add_recipient(self, key, header=None):
|
||||
"""Encrypt the plaintext with the given key.
|
||||
|
||||
:param key: A JWK key or password of appropriate type for the 'alg'
|
||||
provided in the JOSE Headers.
|
||||
:param header: A JSON string representing the per-recipient header.
|
||||
|
||||
:raises ValueError: if the plaintext is missing or not of type bytes.
|
||||
:raises ValueError: if the compression type is unknown.
|
||||
:raises InvalidJWAAlgorithm: if the 'alg' provided in the JOSE
|
||||
headers is missing or unknown, or otherwise not implemented.
|
||||
"""
|
||||
if self.plaintext is None:
|
||||
raise ValueError('Missing plaintext')
|
||||
if not isinstance(self.plaintext, bytes):
|
||||
raise ValueError("Plaintext must be 'bytes'")
|
||||
|
||||
if isinstance(header, dict):
|
||||
header = json_encode(header)
|
||||
|
||||
jh = self._get_jose_header(header)
|
||||
alg, enc = self._get_alg_enc_from_headers(jh)
|
||||
|
||||
rec = {}
|
||||
if header:
|
||||
rec['header'] = header
|
||||
|
||||
wrapped = alg.wrap(key, enc.wrap_key_size, self.cek, jh)
|
||||
self.cek = wrapped['cek']
|
||||
|
||||
if 'ek' in wrapped:
|
||||
rec['encrypted_key'] = wrapped['ek']
|
||||
|
||||
if 'header' in wrapped:
|
||||
h = json_decode(rec.get('header', '{}'))
|
||||
nh = self._merge_headers(h, wrapped['header'])
|
||||
rec['header'] = json_encode(nh)
|
||||
|
||||
if 'ciphertext' not in self.objects:
|
||||
self._encrypt(alg, enc, jh)
|
||||
|
||||
if 'recipients' in self.objects:
|
||||
self.objects['recipients'].append(rec)
|
||||
elif 'encrypted_key' in self.objects or 'header' in self.objects:
|
||||
self.objects['recipients'] = []
|
||||
n = {}
|
||||
if 'encrypted_key' in self.objects:
|
||||
n['encrypted_key'] = self.objects.pop('encrypted_key')
|
||||
if 'header' in self.objects:
|
||||
n['header'] = self.objects.pop('header')
|
||||
self.objects['recipients'].append(n)
|
||||
self.objects['recipients'].append(rec)
|
||||
else:
|
||||
self.objects.update(rec)
|
||||
|
||||
def serialize(self, compact=False):
|
||||
"""Serializes the object into a JWE token.
|
||||
|
||||
:param compact(boolean): if True generates the compact
|
||||
representation, otherwise generates a standard JSON format.
|
||||
|
||||
:raises InvalidJWEOperation: if the object cannot be serialized
|
||||
with the compact representation and `compact` is True.
|
||||
:raises InvalidJWEOperation: if no recipients have been added
|
||||
to the object.
|
||||
|
||||
:return: A json formatted string or a compact representation string
|
||||
:rtype: `str`
|
||||
"""
|
||||
|
||||
if 'ciphertext' not in self.objects:
|
||||
raise InvalidJWEOperation("No available ciphertext")
|
||||
|
||||
if compact:
|
||||
for invalid in 'aad', 'unprotected':
|
||||
if invalid in self.objects:
|
||||
raise InvalidJWEOperation(
|
||||
"Can't use compact encoding when the '%s' parameter "
|
||||
"is set" % invalid)
|
||||
if 'protected' not in self.objects:
|
||||
raise InvalidJWEOperation(
|
||||
"Can't use compact encoding without protected headers")
|
||||
else:
|
||||
ph = json_decode(self.objects['protected'])
|
||||
for required in 'alg', 'enc':
|
||||
if required not in ph:
|
||||
raise InvalidJWEOperation(
|
||||
"Can't use compact encoding, '%s' must be in the "
|
||||
"protected header" % required)
|
||||
if 'recipients' in self.objects:
|
||||
if len(self.objects['recipients']) != 1:
|
||||
raise InvalidJWEOperation("Invalid number of recipients")
|
||||
rec = self.objects['recipients'][0]
|
||||
else:
|
||||
rec = self.objects
|
||||
if 'header' in rec:
|
||||
# The AESGCMKW algorithm generates data (iv, tag) we put in the
|
||||
# per-recipient unprotected header by default. Move it to the
|
||||
# protected header and re-encrypt the payload, as the protected
|
||||
# header is used as additional authenticated data.
|
||||
h = json_decode(rec['header'])
|
||||
ph = json_decode(self.objects['protected'])
|
||||
nph = self._merge_headers(h, ph)
|
||||
self.objects['protected'] = json_encode(nph)
|
||||
jh = self._get_jose_header()
|
||||
alg, enc = self._get_alg_enc_from_headers(jh)
|
||||
self._encrypt(alg, enc, jh)
|
||||
del rec['header']
|
||||
|
||||
return '.'.join([base64url_encode(self.objects['protected']),
|
||||
base64url_encode(rec.get('encrypted_key', '')),
|
||||
base64url_encode(self.objects['iv']),
|
||||
base64url_encode(self.objects['ciphertext']),
|
||||
base64url_encode(self.objects['tag'])])
|
||||
else:
|
||||
obj = self.objects
|
||||
enc = {'ciphertext': base64url_encode(obj['ciphertext']),
|
||||
'iv': base64url_encode(obj['iv']),
|
||||
'tag': base64url_encode(self.objects['tag'])}
|
||||
if 'protected' in obj:
|
||||
enc['protected'] = base64url_encode(obj['protected'])
|
||||
if 'unprotected' in obj:
|
||||
enc['unprotected'] = json_decode(obj['unprotected'])
|
||||
if 'aad' in obj:
|
||||
enc['aad'] = base64url_encode(obj['aad'])
|
||||
if 'recipients' in obj:
|
||||
enc['recipients'] = []
|
||||
for rec in obj['recipients']:
|
||||
e = {}
|
||||
if 'encrypted_key' in rec:
|
||||
e['encrypted_key'] = \
|
||||
base64url_encode(rec['encrypted_key'])
|
||||
if 'header' in rec:
|
||||
e['header'] = json_decode(rec['header'])
|
||||
enc['recipients'].append(e)
|
||||
else:
|
||||
if 'encrypted_key' in obj:
|
||||
enc['encrypted_key'] = \
|
||||
base64url_encode(obj['encrypted_key'])
|
||||
if 'header' in obj:
|
||||
enc['header'] = json_decode(obj['header'])
|
||||
return json_encode(enc)
|
||||
|
||||
def _check_crit(self, crit):
|
||||
for k in crit:
|
||||
if k not in self.header_registry:
|
||||
raise InvalidJWEData('Unknown critical header: "%s"' % k)
|
||||
else:
|
||||
if not self.header_registry[k].supported:
|
||||
raise InvalidJWEData('Unsupported critical header: '
|
||||
'"%s"' % k)
|
||||
|
||||
def _unwrap_decrypt(self, alg, enc, key, enckey, header,
|
||||
aad, iv, ciphertext, tag):
|
||||
cek = alg.unwrap(key, enc.wrap_key_size, enckey, header)
|
||||
data = enc.decrypt(cek, aad, iv, ciphertext, tag)
|
||||
self.decryptlog.append('Success')
|
||||
self.cek = cek
|
||||
return data
|
||||
|
||||
# FIXME: allow to specify which algorithms to accept as valid
|
||||
def _decrypt(self, key, ppe):
|
||||
|
||||
jh = self._get_jose_header(ppe.get('header', None))
|
||||
|
||||
# TODO: allow caller to specify list of headers it understands
|
||||
self._check_crit(jh.get('crit', {}))
|
||||
|
||||
for hdr in jh:
|
||||
if hdr in self.header_registry:
|
||||
if not self.header_registry.check_header(hdr, self):
|
||||
raise InvalidJWEData('Failed header check')
|
||||
|
||||
alg = self._jwa_keymgmt(jh.get('alg', None))
|
||||
enc = self._jwa_enc(jh.get('enc', None))
|
||||
|
||||
aad = base64url_encode(self.objects.get('protected', ''))
|
||||
if 'aad' in self.objects:
|
||||
aad += '.' + base64url_encode(self.objects['aad'])
|
||||
aad = aad.encode('utf-8')
|
||||
|
||||
if isinstance(key, JWKSet):
|
||||
keys = key
|
||||
if 'kid' in self.jose_header:
|
||||
kid_keys = key.get_keys(self.jose_header['kid'])
|
||||
if not kid_keys:
|
||||
raise JWKeyNotFound('Key ID {} not in key set'.format(
|
||||
self.jose_header['kid']))
|
||||
keys = kid_keys
|
||||
|
||||
for k in keys:
|
||||
try:
|
||||
data = self._unwrap_decrypt(alg, enc, k,
|
||||
ppe.get('encrypted_key', b''),
|
||||
jh, aad, self.objects['iv'],
|
||||
self.objects['ciphertext'],
|
||||
self.objects['tag'])
|
||||
self.decryptlog.append("Success")
|
||||
break
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
keyid = k.get('kid', k.thumbprint())
|
||||
self.decryptlog.append('Key [{}] failed: [{}]'.format(
|
||||
keyid, repr(e)))
|
||||
|
||||
if "Success" not in self.decryptlog:
|
||||
raise JWKeyNotFound('No working key found in key set')
|
||||
else:
|
||||
data = self._unwrap_decrypt(alg, enc, key,
|
||||
ppe.get('encrypted_key', b''),
|
||||
jh, aad, self.objects['iv'],
|
||||
self.objects['ciphertext'],
|
||||
self.objects['tag'])
|
||||
|
||||
compress = jh.get('zip', None)
|
||||
if compress == 'DEF':
|
||||
if len(data) > default_max_compressed_size:
|
||||
raise InvalidJWEData(
|
||||
'Compressed data exceeds maximum allowed'
|
||||
'size' + f' ({default_max_compressed_size})')
|
||||
self.plaintext = zlib.decompress(data, -zlib.MAX_WBITS)
|
||||
elif compress is None:
|
||||
self.plaintext = data
|
||||
else:
|
||||
raise ValueError('Unknown compression')
|
||||
|
||||
def decrypt(self, key):
|
||||
"""Decrypt a JWE token.
|
||||
|
||||
:param key: The (:class:`jwcrypto.jwk.JWK`) decryption key.
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) decryption key,
|
||||
or a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed
|
||||
by the 'kid' header or (deprecated) a string containing a password.
|
||||
|
||||
:raises InvalidJWEOperation: if the key is not a JWK object.
|
||||
:raises InvalidJWEData: if the ciphertext can't be decrypted or
|
||||
the object is otherwise malformed.
|
||||
:raises JWKeyNotFound: if key is a JWKSet and the key is not found.
|
||||
"""
|
||||
|
||||
if 'ciphertext' not in self.objects:
|
||||
raise InvalidJWEOperation("No available ciphertext")
|
||||
self.decryptlog = []
|
||||
missingkey = False
|
||||
|
||||
if 'recipients' in self.objects:
|
||||
for rec in self.objects['recipients']:
|
||||
try:
|
||||
self._decrypt(key, rec)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
if isinstance(e, JWKeyNotFound):
|
||||
missingkey = True
|
||||
self.decryptlog.append('Failed: [%s]' % repr(e))
|
||||
else:
|
||||
try:
|
||||
self._decrypt(key, self.objects)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
if isinstance(e, JWKeyNotFound):
|
||||
missingkey = True
|
||||
self.decryptlog.append('Failed: [%s]' % repr(e))
|
||||
|
||||
if not self.plaintext:
|
||||
if missingkey:
|
||||
raise JWKeyNotFound("Key Not found in JWKSet")
|
||||
raise InvalidJWEData('No recipient matched the provided '
|
||||
'key' + repr(self.decryptlog))
|
||||
|
||||
def deserialize(self, raw_jwe, key=None):
|
||||
"""Deserialize a JWE token.
|
||||
|
||||
NOTE: Destroys any current status and tries to import the raw
|
||||
JWE provided.
|
||||
|
||||
If a key is provided a decryption step will be attempted after
|
||||
the object is successfully deserialized.
|
||||
|
||||
:param raw_jwe: a 'raw' JWE token (JSON Encoded or Compact
|
||||
notation) string.
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) decryption key,
|
||||
or a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed
|
||||
by the 'kid' header or (deprecated) a string containing a password
|
||||
(optional).
|
||||
|
||||
:raises InvalidJWEData: if the raw object is an invalid JWE token.
|
||||
:raises InvalidJWEOperation: if the decryption fails.
|
||||
"""
|
||||
|
||||
self.objects = {}
|
||||
self.plaintext = None
|
||||
self.cek = None
|
||||
|
||||
o = {}
|
||||
try:
|
||||
try:
|
||||
djwe = json_decode(raw_jwe)
|
||||
o['iv'] = base64url_decode(djwe['iv'])
|
||||
o['ciphertext'] = base64url_decode(djwe['ciphertext'])
|
||||
o['tag'] = base64url_decode(djwe['tag'])
|
||||
if 'protected' in djwe:
|
||||
p = base64url_decode(djwe['protected'])
|
||||
o['protected'] = p.decode('utf-8')
|
||||
if 'unprotected' in djwe:
|
||||
o['unprotected'] = json_encode(djwe['unprotected'])
|
||||
if 'aad' in djwe:
|
||||
o['aad'] = base64url_decode(djwe['aad'])
|
||||
if 'recipients' in djwe:
|
||||
o['recipients'] = []
|
||||
for rec in djwe['recipients']:
|
||||
e = {}
|
||||
if 'encrypted_key' in rec:
|
||||
e['encrypted_key'] = \
|
||||
base64url_decode(rec['encrypted_key'])
|
||||
if 'header' in rec:
|
||||
e['header'] = json_encode(rec['header'])
|
||||
o['recipients'].append(e)
|
||||
else:
|
||||
if 'encrypted_key' in djwe:
|
||||
o['encrypted_key'] = \
|
||||
base64url_decode(djwe['encrypted_key'])
|
||||
if 'header' in djwe:
|
||||
o['header'] = json_encode(djwe['header'])
|
||||
|
||||
except ValueError as e:
|
||||
data = raw_jwe.split('.')
|
||||
if len(data) != 5:
|
||||
raise InvalidJWEData() from e
|
||||
p = base64url_decode(data[0])
|
||||
o['protected'] = p.decode('utf-8')
|
||||
ekey = base64url_decode(data[1])
|
||||
if ekey != b'':
|
||||
o['encrypted_key'] = base64url_decode(data[1])
|
||||
o['iv'] = base64url_decode(data[2])
|
||||
o['ciphertext'] = base64url_decode(data[3])
|
||||
o['tag'] = base64url_decode(data[4])
|
||||
|
||||
self.objects = o
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
raise InvalidJWEData('Invalid format', repr(e)) from e
|
||||
|
||||
if key:
|
||||
self.decrypt(key)
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
if not self.plaintext:
|
||||
raise InvalidJWEOperation("Plaintext not available")
|
||||
return self.plaintext
|
||||
|
||||
@property
|
||||
def jose_header(self):
|
||||
jh = self._get_jose_header(self.objects.get('header'))
|
||||
if len(jh) == 0:
|
||||
raise InvalidJWEOperation("JOSE Header not available")
|
||||
return jh
|
||||
|
||||
@classmethod
|
||||
def from_jose_token(cls, token):
|
||||
"""Creates a JWE object from a serialized JWE token.
|
||||
|
||||
:param token: A string with the json or compat representation
|
||||
of the token.
|
||||
|
||||
:raises InvalidJWEData: if the raw object is an invalid JWE token.
|
||||
|
||||
:return: A JWE token
|
||||
:rtype: JWE
|
||||
"""
|
||||
|
||||
obj = cls()
|
||||
obj.deserialize(token)
|
||||
return obj
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JWE):
|
||||
return False
|
||||
try:
|
||||
return self.serialize() == other.serialize()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
data1 = {'plaintext': self.plaintext}
|
||||
data1.update(self.objects)
|
||||
data2 = {'plaintext': other.plaintext}
|
||||
data2.update(other.objects)
|
||||
return data1 == data2
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return self.serialize()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
return f'JWE.from_json_token("{self.serialize()}")'
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plaintext = repr(self.plaintext)
|
||||
protected = self.objects.get('protected')
|
||||
unprotected = self.objects.get('unprotected')
|
||||
aad = self.objects.get('aad')
|
||||
algs = self._allowed_algs
|
||||
return f'JWE(plaintext={plaintext}, ' + \
|
||||
f'protected={protected}, ' + \
|
||||
f'unprotected={unprotected}, ' + \
|
||||
f'aad={aad}, algs={algs})'
|
||||
1414
venv/lib/python3.11/site-packages/jwcrypto/jwk.py
Normal file
1414
venv/lib/python3.11/site-packages/jwcrypto/jwk.py
Normal file
File diff suppressed because it is too large
Load Diff
721
venv/lib/python3.11/site-packages/jwcrypto/jws.py
Normal file
721
venv/lib/python3.11/site-packages/jwcrypto/jws.py
Normal file
@@ -0,0 +1,721 @@
|
||||
# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
|
||||
|
||||
from jwcrypto.common import JWException, JWKeyNotFound
|
||||
from jwcrypto.common import JWSEHeaderParameter, JWSEHeaderRegistry
|
||||
from jwcrypto.common import base64url_decode, base64url_encode
|
||||
from jwcrypto.common import json_decode, json_encode
|
||||
from jwcrypto.jwa import JWA
|
||||
from jwcrypto.jwk import JWK, JWKSet
|
||||
|
||||
JWSHeaderRegistry = {
|
||||
'alg': JWSEHeaderParameter('Algorithm', False, True, None),
|
||||
'jku': JWSEHeaderParameter('JWK Set URL', False, False, None),
|
||||
'jwk': JWSEHeaderParameter('JSON Web Key', False, False, None),
|
||||
'kid': JWSEHeaderParameter('Key ID', False, True, None),
|
||||
'x5u': JWSEHeaderParameter('X.509 URL', False, False, None),
|
||||
'x5c': JWSEHeaderParameter('X.509 Certificate Chain', False, False, None),
|
||||
'x5t': JWSEHeaderParameter(
|
||||
'X.509 Certificate SHA-1 Thumbprint', False, False, None),
|
||||
'x5t#S256': JWSEHeaderParameter(
|
||||
'X.509 Certificate SHA-256 Thumbprint', False, False, None),
|
||||
'typ': JWSEHeaderParameter('Type', False, True, None),
|
||||
'cty': JWSEHeaderParameter('Content Type', False, True, None),
|
||||
'crit': JWSEHeaderParameter('Critical', True, True, None),
|
||||
'b64': JWSEHeaderParameter('Base64url-Encode Payload', True, True, None)
|
||||
}
|
||||
"""Registry of valid header parameters"""
|
||||
|
||||
default_allowed_algs = [
|
||||
'HS256', 'HS384', 'HS512',
|
||||
'RS256', 'RS384', 'RS512',
|
||||
'ES256', 'ES384', 'ES512',
|
||||
'PS256', 'PS384', 'PS512',
|
||||
'EdDSA', 'ES256K']
|
||||
"""Default allowed algorithms"""
|
||||
|
||||
|
||||
class InvalidJWSSignature(JWException):
|
||||
"""Invalid JWS Signature.
|
||||
|
||||
This exception is raised when a signature cannot be validated.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Unknown Signature Verification Failure'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(InvalidJWSSignature, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWSObject(JWException):
|
||||
"""Invalid JWS Object.
|
||||
|
||||
This exception is raised when the JWS Object is invalid and/or
|
||||
improperly formatted.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = 'Invalid JWS Object'
|
||||
if message:
|
||||
msg += ' [%s]' % message
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(InvalidJWSObject, self).__init__(msg)
|
||||
|
||||
|
||||
class InvalidJWSOperation(JWException):
|
||||
"""Invalid JWS Object.
|
||||
|
||||
This exception is raised when a requested operation cannot
|
||||
be execute due to unsatisfied conditions.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = message
|
||||
else:
|
||||
msg = 'Unknown Operation Failure'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(InvalidJWSOperation, self).__init__(msg)
|
||||
|
||||
|
||||
class JWSCore:
|
||||
"""The inner JWS Core object.
|
||||
|
||||
This object SHOULD NOT be used directly, the JWS object should be
|
||||
used instead as JWS perform necessary checks on the validity of
|
||||
the object and requested operations.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, alg, key, header, payload, algs=None):
|
||||
"""Core JWS token handling.
|
||||
|
||||
:param alg: The algorithm used to produce the signature.
|
||||
See RFC 7518
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
|
||||
a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed by the
|
||||
'kid' header. A JWKSet is allowed only for verification operations.
|
||||
:param header: A JSON string representing the protected header.
|
||||
:param payload(bytes): An arbitrary value
|
||||
:param algs: An optional list of allowed algorithms
|
||||
|
||||
:raises ValueError: if the key is not a (:class:`jwcrypto.jwk.JWK`)
|
||||
:raises InvalidJWAAlgorithm: if the algorithm is not valid, is
|
||||
unknown or otherwise not yet implemented.
|
||||
:raises InvalidJWSOperation: if the algorithm is not allowed.
|
||||
"""
|
||||
self.alg = alg
|
||||
self.engine = self._jwa(alg, algs)
|
||||
self.key = key
|
||||
|
||||
if header is not None:
|
||||
if isinstance(header, dict):
|
||||
header = json_encode(header)
|
||||
# Make sure this is always a deep copy of the dict
|
||||
self.header = json_decode(header)
|
||||
|
||||
self.protected = base64url_encode(header.encode('utf-8'))
|
||||
else:
|
||||
self.header = {}
|
||||
self.protected = ''
|
||||
self.payload = self._payload(payload)
|
||||
|
||||
def _jwa(self, name, allowed):
|
||||
if allowed is None:
|
||||
allowed = default_allowed_algs
|
||||
if name not in allowed:
|
||||
raise InvalidJWSOperation('Algorithm not allowed')
|
||||
return JWA.signing_alg(name)
|
||||
|
||||
def _payload(self, payload):
|
||||
if self.header.get('b64', True):
|
||||
return base64url_encode(payload).encode('utf-8')
|
||||
else:
|
||||
if isinstance(payload, bytes):
|
||||
return payload
|
||||
else:
|
||||
return payload.encode('utf-8')
|
||||
|
||||
def sign(self):
|
||||
"""Generates a signature"""
|
||||
if not isinstance(self.key, JWK):
|
||||
raise ValueError('key is not a JWK object')
|
||||
sigin = b'.'.join([self.protected.encode('utf-8'),
|
||||
self.payload])
|
||||
signature = self.engine.sign(self.key, sigin)
|
||||
return {'protected': self.protected,
|
||||
'payload': self.payload,
|
||||
'signature': base64url_encode(signature)}
|
||||
|
||||
def verify(self, signature):
|
||||
"""Verifies a signature
|
||||
|
||||
:raises InvalidJWSSignature: if the verification fails.
|
||||
|
||||
:return: Returns True or an Exception
|
||||
:rtype: `bool`
|
||||
"""
|
||||
try:
|
||||
sigin = b'.'.join([self.protected.encode('utf-8'),
|
||||
self.payload])
|
||||
self.engine.verify(self.key, sigin, signature)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
raise InvalidJWSSignature('Verification failed') from e
|
||||
return True
|
||||
|
||||
|
||||
class JWS:
|
||||
"""JSON Web Signature object
|
||||
|
||||
This object represent a JWS token.
|
||||
"""
|
||||
|
||||
def __init__(self, payload=None, header_registry=None):
|
||||
"""Creates a JWS object.
|
||||
|
||||
:param payload(bytes): An arbitrary value (optional).
|
||||
:param header_registry: Optional additions to the header registry
|
||||
"""
|
||||
self.objects = {}
|
||||
self.objects['payload'] = payload
|
||||
self.verifylog = None
|
||||
self._allowed_algs = None
|
||||
self.header_registry = JWSEHeaderRegistry(JWSHeaderRegistry)
|
||||
if header_registry:
|
||||
self.header_registry.update(header_registry)
|
||||
|
||||
@property
|
||||
def allowed_algs(self):
|
||||
"""Allowed algorithms.
|
||||
|
||||
The list of allowed algorithms.
|
||||
Can be changed by setting a list of algorithm names.
|
||||
"""
|
||||
|
||||
if self._allowed_algs:
|
||||
return self._allowed_algs
|
||||
else:
|
||||
return default_allowed_algs
|
||||
|
||||
@allowed_algs.setter
|
||||
def allowed_algs(self, algs):
|
||||
if not isinstance(algs, list):
|
||||
raise TypeError('Allowed Algs must be a list')
|
||||
self._allowed_algs = algs
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.objects.get('valid', False)
|
||||
|
||||
# TODO: allow caller to specify list of headers it understands
|
||||
# FIXME: Merge and check to be changed to two separate functions
|
||||
def _merge_check_headers(self, protected, *headers):
|
||||
header = None
|
||||
crit = []
|
||||
if protected is not None:
|
||||
if 'crit' in protected:
|
||||
crit = protected['crit']
|
||||
# Check immediately if we support these critical headers
|
||||
for k in crit:
|
||||
if k not in self.header_registry:
|
||||
raise InvalidJWSObject(
|
||||
'Unknown critical header: "%s"' % k)
|
||||
else:
|
||||
if not self.header_registry[k].supported:
|
||||
raise InvalidJWSObject(
|
||||
'Unsupported critical header: "%s"' % k)
|
||||
header = protected
|
||||
if 'b64' in header:
|
||||
if not isinstance(header['b64'], bool):
|
||||
raise InvalidJWSObject('b64 header must be a boolean')
|
||||
|
||||
for hn in headers:
|
||||
if hn is None:
|
||||
continue
|
||||
if header is None:
|
||||
header = {}
|
||||
for h in list(hn.keys()):
|
||||
if h in self.header_registry:
|
||||
if self.header_registry[h].mustprotect:
|
||||
raise InvalidJWSObject('"%s" must be protected' % h)
|
||||
if h in header:
|
||||
raise InvalidJWSObject('Duplicate header: "%s"' % h)
|
||||
header.update(hn)
|
||||
|
||||
for k in crit:
|
||||
if k not in header:
|
||||
raise InvalidJWSObject('Missing critical header "%s"' % k)
|
||||
|
||||
return header
|
||||
|
||||
def _verify(self, alg, key, payload, signature, protected, header=None):
|
||||
p = {}
|
||||
# verify it is a valid JSON object and decode
|
||||
if protected is not None:
|
||||
p = json_decode(protected)
|
||||
if not isinstance(p, dict):
|
||||
raise InvalidJWSSignature('Invalid Protected header')
|
||||
# merge headers, and verify there are no duplicates
|
||||
if header:
|
||||
if not isinstance(header, dict):
|
||||
raise InvalidJWSSignature('Invalid Unprotected header')
|
||||
|
||||
# Merge and check (critical) headers
|
||||
chk_hdrs = self._merge_check_headers(p, header)
|
||||
for hdr in chk_hdrs:
|
||||
if hdr in self.header_registry:
|
||||
if not self.header_registry.check_header(hdr, self):
|
||||
raise InvalidJWSSignature('Failed header check')
|
||||
|
||||
# check 'alg' is present
|
||||
if alg is None and 'alg' not in p:
|
||||
raise InvalidJWSSignature('No "alg" in headers')
|
||||
if alg:
|
||||
if 'alg' in p and alg != p['alg']:
|
||||
raise InvalidJWSSignature(
|
||||
'"alg" mismatch, requested'
|
||||
f''' "{alg}", found "{p['alg']}"'''
|
||||
)
|
||||
resulting_alg = alg
|
||||
else:
|
||||
resulting_alg = p['alg']
|
||||
|
||||
# the following will verify the "alg" is supported and the signature
|
||||
# verifies
|
||||
if isinstance(key, JWK):
|
||||
signer = JWSCore(resulting_alg, key, protected,
|
||||
payload, self._allowed_algs)
|
||||
signer.verify(signature)
|
||||
self.verifylog.append("Success")
|
||||
elif isinstance(key, JWKSet):
|
||||
keys = key
|
||||
if 'kid' in self.jose_header:
|
||||
kid_keys = key.get_keys(self.jose_header['kid'])
|
||||
if not kid_keys:
|
||||
raise JWKeyNotFound('Key ID {} not in key set'.format(
|
||||
self.jose_header['kid']))
|
||||
keys = kid_keys
|
||||
|
||||
for k in keys:
|
||||
try:
|
||||
signer2 = JWSCore(
|
||||
resulting_alg, k, protected,
|
||||
payload, self._allowed_algs
|
||||
)
|
||||
signer2.verify(signature)
|
||||
self.verifylog.append("Success")
|
||||
break
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
keyid = k.get('kid', k.thumbprint())
|
||||
self.verifylog.append('Key [{}] failed: [{}]'.format(
|
||||
keyid, repr(e)))
|
||||
if "Success" not in self.verifylog:
|
||||
raise JWKeyNotFound('No working key found in key set')
|
||||
else:
|
||||
raise ValueError("Unrecognized key type")
|
||||
|
||||
# Helper to deal with detached payloads in verification
|
||||
def _get_obj_payload(self, obj, dp):
|
||||
op = obj.get('payload')
|
||||
if dp is not None:
|
||||
if op is None or len(op) == 0:
|
||||
return dp
|
||||
else:
|
||||
raise InvalidJWSOperation('Object Payload present but'
|
||||
' Detached Payload provided')
|
||||
return op
|
||||
|
||||
def verify(self, key, alg=None, detached_payload=None):
|
||||
"""Verifies a JWS token.
|
||||
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
|
||||
a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed by the
|
||||
'kid' header.
|
||||
:param alg: The signing algorithm (optional). Usually the algorithm
|
||||
is known as it is provided with the JOSE Headers of the token.
|
||||
:param detached_payload: A detached payload to verify the signature
|
||||
against. Only valid for tokens that are not carrying a payload.
|
||||
|
||||
:raises InvalidJWSSignature: if the verification fails.
|
||||
:raises InvalidJWSOperation: if a detached_payload is provided but
|
||||
an object payload exists
|
||||
:raises JWKeyNotFound: if key is a JWKSet and the key is not found.
|
||||
"""
|
||||
|
||||
self.verifylog = []
|
||||
self.objects['valid'] = False
|
||||
obj = self.objects
|
||||
missingkey = False
|
||||
if 'signature' in obj:
|
||||
payload = self._get_obj_payload(obj, detached_payload)
|
||||
try:
|
||||
self._verify(alg, key,
|
||||
payload,
|
||||
obj['signature'],
|
||||
obj.get('protected', None),
|
||||
obj.get('header', None))
|
||||
obj['valid'] = True
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
if isinstance(e, JWKeyNotFound):
|
||||
missingkey = True
|
||||
self.verifylog.append('Failed: [%s]' % repr(e))
|
||||
|
||||
elif 'signatures' in obj:
|
||||
payload = self._get_obj_payload(obj, detached_payload)
|
||||
for o in obj['signatures']:
|
||||
try:
|
||||
self._verify(alg, key,
|
||||
payload,
|
||||
o['signature'],
|
||||
o.get('protected', None),
|
||||
o.get('header', None))
|
||||
# Ok if at least one verifies
|
||||
obj['valid'] = True
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
if isinstance(e, JWKeyNotFound):
|
||||
missingkey = True
|
||||
self.verifylog.append('Failed: [%s]' % repr(e))
|
||||
else:
|
||||
raise InvalidJWSSignature('No signatures available')
|
||||
|
||||
if not self.is_valid:
|
||||
if missingkey:
|
||||
raise JWKeyNotFound('No working key found in key set')
|
||||
raise InvalidJWSSignature('Verification failed for all '
|
||||
'signatures' + repr(self.verifylog))
|
||||
|
||||
def _deserialize_signature(self, s):
|
||||
o = {'signature': base64url_decode(str(s['signature']))}
|
||||
if 'protected' in s:
|
||||
p = base64url_decode(str(s['protected']))
|
||||
o['protected'] = p.decode('utf-8')
|
||||
if 'header' in s:
|
||||
o['header'] = s['header']
|
||||
return o
|
||||
|
||||
def _deserialize_b64(self, o, protected):
|
||||
if protected is None:
|
||||
b64n = None
|
||||
else:
|
||||
p = json_decode(protected)
|
||||
b64n = p.get('b64')
|
||||
if b64n is not None:
|
||||
if not isinstance(b64n, bool):
|
||||
raise InvalidJWSObject('b64 header must be boolean')
|
||||
b64 = o.get('b64')
|
||||
if b64 == b64n:
|
||||
return
|
||||
elif b64 is None:
|
||||
o['b64'] = b64n
|
||||
else:
|
||||
raise InvalidJWSObject('conflicting b64 values')
|
||||
|
||||
def deserialize(self, raw_jws, key=None, alg=None):
|
||||
"""Deserialize a JWS token.
|
||||
|
||||
NOTE: Destroys any current status and tries to import the raw
|
||||
JWS provided.
|
||||
|
||||
If a key is provided a verification step will be attempted after
|
||||
the object is successfully deserialized.
|
||||
|
||||
:param raw_jws: a 'raw' JWS token (JSON Encoded or Compact
|
||||
notation) string.
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
|
||||
a (:class:`jwcrypto.jwk.JWKSet`) that contains a key indexed by the
|
||||
'kid' header (optional).
|
||||
:param alg: The signing algorithm (optional). Usually the algorithm
|
||||
is known as it is provided with the JOSE Headers of the token.
|
||||
|
||||
:raises InvalidJWSObject: if the raw object is an invalid JWS token.
|
||||
:raises InvalidJWSSignature: if the verification fails.
|
||||
:raises JWKeyNotFound: if key is a JWKSet and the key is not found.
|
||||
"""
|
||||
self.objects = {}
|
||||
o = {}
|
||||
try:
|
||||
try:
|
||||
djws = json_decode(raw_jws)
|
||||
if 'signatures' in djws:
|
||||
o['signatures'] = []
|
||||
for s in djws['signatures']:
|
||||
os = self._deserialize_signature(s)
|
||||
o['signatures'].append(os)
|
||||
self._deserialize_b64(o, os.get('protected'))
|
||||
else:
|
||||
o = self._deserialize_signature(djws)
|
||||
self._deserialize_b64(o, o.get('protected'))
|
||||
|
||||
if 'payload' in djws:
|
||||
if o.get('b64', True):
|
||||
o['payload'] = base64url_decode(str(djws['payload']))
|
||||
else:
|
||||
o['payload'] = djws['payload']
|
||||
|
||||
except ValueError:
|
||||
data = raw_jws.split('.')
|
||||
if len(data) != 3:
|
||||
raise InvalidJWSObject('Unrecognized'
|
||||
' representation') from None
|
||||
p = base64url_decode(str(data[0]))
|
||||
if len(p) > 0:
|
||||
o['protected'] = p.decode('utf-8')
|
||||
self._deserialize_b64(o, o['protected'])
|
||||
o['payload'] = base64url_decode(str(data[1]))
|
||||
o['signature'] = base64url_decode(str(data[2]))
|
||||
|
||||
self.objects = o
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
raise InvalidJWSObject('Invalid format') from e
|
||||
|
||||
if key:
|
||||
self.verify(key, alg)
|
||||
|
||||
def add_signature(self, key, alg=None, protected=None, header=None):
|
||||
"""Adds a new signature to the object.
|
||||
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) key of appropriate for
|
||||
the "alg" provided.
|
||||
:param alg: An optional algorithm name. If already provided as an
|
||||
element of the protected or unprotected header it can be safely
|
||||
omitted.
|
||||
:param protected: The Protected Header (optional)
|
||||
:param header: The Unprotected Header (optional)
|
||||
|
||||
:raises InvalidJWSObject: if invalid headers are provided.
|
||||
:raises ValueError: if the key is not a (:class:`jwcrypto.jwk.JWK`)
|
||||
:raises ValueError: if the algorithm is missing or is not provided
|
||||
by one of the headers.
|
||||
:raises InvalidJWAAlgorithm: if the algorithm is not valid, is
|
||||
unknown or otherwise not yet implemented.
|
||||
"""
|
||||
|
||||
b64 = True
|
||||
|
||||
if protected:
|
||||
if isinstance(protected, dict):
|
||||
protected = json_encode(protected)
|
||||
# Make sure p is always a deep copy of the dict
|
||||
p = json_decode(protected)
|
||||
else:
|
||||
p = dict()
|
||||
|
||||
# If b64 is present we must enforce criticality
|
||||
if 'b64' in list(p.keys()):
|
||||
crit = p.get('crit', [])
|
||||
if 'b64' not in crit:
|
||||
raise InvalidJWSObject('b64 header must always be critical')
|
||||
b64 = p['b64']
|
||||
|
||||
if 'b64' in self.objects:
|
||||
if b64 != self.objects['b64']:
|
||||
raise InvalidJWSObject('Mixed b64 headers on signatures')
|
||||
|
||||
h = None
|
||||
if header:
|
||||
if isinstance(header, dict):
|
||||
header = json_encode(header)
|
||||
# Make sure h is always a deep copy of the dict
|
||||
h = json_decode(header)
|
||||
|
||||
p = self._merge_check_headers(p, h)
|
||||
|
||||
if 'alg' in p:
|
||||
if alg is None:
|
||||
alg = p['alg']
|
||||
elif alg != p['alg']:
|
||||
raise ValueError('"alg" value mismatch, specified "alg" '
|
||||
'does not match JOSE header value')
|
||||
|
||||
if alg is None:
|
||||
raise ValueError('"alg" not specified')
|
||||
|
||||
c = JWSCore(
|
||||
alg, key, protected, self.objects.get('payload'),
|
||||
self.allowed_algs
|
||||
)
|
||||
sig = c.sign()
|
||||
|
||||
o = {
|
||||
'signature': base64url_decode(sig['signature']),
|
||||
'valid': True,
|
||||
}
|
||||
if protected:
|
||||
o['protected'] = protected
|
||||
if header:
|
||||
o['header'] = h
|
||||
|
||||
if 'signatures' in self.objects:
|
||||
self.objects['signatures'].append(o)
|
||||
elif 'signature' in self.objects:
|
||||
self.objects['signatures'] = []
|
||||
n = {'signature': self.objects.pop('signature')}
|
||||
if 'protected' in self.objects:
|
||||
n['protected'] = self.objects.pop('protected')
|
||||
if 'header' in self.objects:
|
||||
n['header'] = self.objects.pop('header')
|
||||
if 'valid' in self.objects:
|
||||
n['valid'] = self.objects.pop('valid')
|
||||
self.objects['signatures'].append(n)
|
||||
self.objects['signatures'].append(o)
|
||||
else:
|
||||
self.objects.update(o)
|
||||
self.objects['b64'] = b64
|
||||
|
||||
def serialize(self, compact=False):
|
||||
"""Serializes the object into a JWS token.
|
||||
|
||||
:param compact(boolean): if True generates the compact
|
||||
representation, otherwise generates a standard JSON format.
|
||||
|
||||
:raises InvalidJWSOperation: if the object cannot serialized
|
||||
with the compact representation and `compact` is True.
|
||||
:raises InvalidJWSSignature: if no signature has been added
|
||||
to the object, or no valid signature can be found.
|
||||
|
||||
:return: A json formatted string or a compact representation string
|
||||
:rtype: `str`
|
||||
"""
|
||||
if compact:
|
||||
if 'signatures' in self.objects:
|
||||
raise InvalidJWSOperation("Can't use compact encoding with "
|
||||
"multiple signatures")
|
||||
if 'signature' not in self.objects:
|
||||
raise InvalidJWSSignature("No available signature")
|
||||
if not self.objects.get('valid', False):
|
||||
raise InvalidJWSSignature("No valid signature found")
|
||||
if 'protected' in self.objects:
|
||||
p = json_decode(self.objects['protected'])
|
||||
if 'alg' not in p:
|
||||
raise InvalidJWSOperation("Compact encoding must carry "
|
||||
"'alg' in protected header")
|
||||
protected = base64url_encode(self.objects['protected'])
|
||||
else:
|
||||
raise InvalidJWSOperation("Can't use compact encoding "
|
||||
"without protected header")
|
||||
if self.objects.get('payload'):
|
||||
if self.objects.get('b64', True):
|
||||
payload = base64url_encode(self.objects['payload'])
|
||||
else:
|
||||
if isinstance(self.objects['payload'], bytes):
|
||||
payload = self.objects['payload'].decode('utf-8')
|
||||
else:
|
||||
payload = self.objects['payload']
|
||||
if '.' in payload:
|
||||
raise InvalidJWSOperation(
|
||||
"Can't use compact encoding with unencoded "
|
||||
"payload that uses the . character")
|
||||
else:
|
||||
payload = ''
|
||||
return '.'.join([protected, payload,
|
||||
base64url_encode(self.objects['signature'])])
|
||||
else:
|
||||
obj = self.objects
|
||||
sig = {}
|
||||
payload = self.objects.get('payload', '')
|
||||
if self.objects.get('b64', True):
|
||||
sig['payload'] = base64url_encode(payload)
|
||||
else:
|
||||
sig['payload'] = payload
|
||||
if 'signature' in obj:
|
||||
if not obj.get('valid', False):
|
||||
raise InvalidJWSSignature("No valid signature found")
|
||||
sig['signature'] = base64url_encode(obj['signature'])
|
||||
if 'protected' in obj:
|
||||
sig['protected'] = base64url_encode(obj['protected'])
|
||||
if 'header' in obj:
|
||||
sig['header'] = obj['header']
|
||||
elif 'signatures' in obj:
|
||||
sig['signatures'] = []
|
||||
for o in obj['signatures']:
|
||||
if not o.get('valid', False):
|
||||
continue
|
||||
s = {'signature': base64url_encode(o['signature'])}
|
||||
if 'protected' in o:
|
||||
s['protected'] = base64url_encode(o['protected'])
|
||||
if 'header' in o:
|
||||
s['header'] = o['header']
|
||||
sig['signatures'].append(s)
|
||||
if len(sig['signatures']) == 0:
|
||||
raise InvalidJWSSignature("No valid signature found")
|
||||
else:
|
||||
raise InvalidJWSSignature("No available signature")
|
||||
return json_encode(sig)
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
if not self.is_valid:
|
||||
raise InvalidJWSOperation("Payload not verified")
|
||||
return self.objects.get('payload')
|
||||
|
||||
def detach_payload(self):
|
||||
self.objects.pop('payload', None)
|
||||
|
||||
@property
|
||||
def jose_header(self):
|
||||
obj = self.objects
|
||||
if 'signature' in obj:
|
||||
if 'protected' in obj:
|
||||
p = json_decode(obj['protected'])
|
||||
else:
|
||||
p = None
|
||||
return self._merge_check_headers(p, obj.get('header', {}))
|
||||
elif 'signatures' in self.objects:
|
||||
jhl = []
|
||||
for o in obj['signatures']:
|
||||
jh = {}
|
||||
if 'protected' in o:
|
||||
p = json_decode(o['protected'])
|
||||
else:
|
||||
p = None
|
||||
jh = self._merge_check_headers(p, o.get('header', {}))
|
||||
jhl.append(jh)
|
||||
return jhl
|
||||
else:
|
||||
raise InvalidJWSOperation("JOSE Header(s) not available")
|
||||
|
||||
@classmethod
|
||||
def from_jose_token(cls, token):
|
||||
"""Creates a JWS object from a serialized JWS token.
|
||||
|
||||
:param token: A string with the json or compat representation
|
||||
of the token.
|
||||
|
||||
:raises InvalidJWSObject: if the raw object is an invalid JWS token.
|
||||
|
||||
:return: A JWS token
|
||||
:rtype: JWS
|
||||
"""
|
||||
|
||||
obj = cls()
|
||||
obj.deserialize(token)
|
||||
return obj
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JWS):
|
||||
return False
|
||||
try:
|
||||
return self.serialize() == other.serialize()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.objects == other.objects
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return self.serialize()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
return f'JWS.from_json_token("{self.serialize()}")'
|
||||
except Exception: # pylint: disable=broad-except
|
||||
payload = self.objects['payload'].decode('utf-8')
|
||||
return f'JWS(payload={payload})'
|
||||
740
venv/lib/python3.11/site-packages/jwcrypto/jwt.py
Normal file
740
venv/lib/python3.11/site-packages/jwcrypto/jwt.py
Normal file
@@ -0,0 +1,740 @@
|
||||
# Copyright (C) 2015 JWCrypto Project Contributors - see LICENSE file
|
||||
|
||||
import copy
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from jwcrypto.common import JWException, JWKeyNotFound
|
||||
from jwcrypto.common import json_decode, json_encode
|
||||
from jwcrypto.jwe import JWE
|
||||
from jwcrypto.jwe import default_allowed_algs as jwe_algs
|
||||
from jwcrypto.jwk import JWK, JWKSet
|
||||
from jwcrypto.jws import JWS
|
||||
from jwcrypto.jws import default_allowed_algs as jws_algs
|
||||
|
||||
|
||||
# RFC 7519 - 4.1
|
||||
# name: description
|
||||
JWTClaimsRegistry = {'iss': 'Issuer',
|
||||
'sub': 'Subject',
|
||||
'aud': 'Audience',
|
||||
'exp': 'Expiration Time',
|
||||
'nbf': 'Not Before',
|
||||
'iat': 'Issued At',
|
||||
'jti': 'JWT ID'}
|
||||
"""Registry of RFC 7519 defined claims"""
|
||||
|
||||
|
||||
# do not use this unless you know about CVE-2022-3102
|
||||
JWT_expect_type = True
|
||||
"""This module parameter can disable the use of the expectation
|
||||
feature that has been introduced to fix CVE-2022-3102. This knob
|
||||
has been added as a workaround for applications that can't be
|
||||
immediately refactored to deal with the change in behavior but it
|
||||
is considered deprecated and will be removed in a future release.
|
||||
"""
|
||||
|
||||
|
||||
class JWTExpired(JWException):
|
||||
"""JSON Web Token is expired.
|
||||
|
||||
This exception is raised when a token is expired according to its claims.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Token expired'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTExpired, self).__init__(msg)
|
||||
|
||||
|
||||
class JWTNotYetValid(JWException):
|
||||
"""JSON Web Token is not yet valid.
|
||||
|
||||
This exception is raised when a token is not valid yet according to its
|
||||
claims.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Token not yet valid'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTNotYetValid, self).__init__(msg)
|
||||
|
||||
|
||||
class JWTMissingClaim(JWException):
|
||||
"""JSON Web Token claim is invalid.
|
||||
|
||||
This exception is raised when a claim does not match the expected value.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Invalid Claim Value'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTMissingClaim, self).__init__(msg)
|
||||
|
||||
|
||||
class JWTInvalidClaimValue(JWException):
|
||||
"""JSON Web Token claim is invalid.
|
||||
|
||||
This exception is raised when a claim does not match the expected value.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Invalid Claim Value'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTInvalidClaimValue, self).__init__(msg)
|
||||
|
||||
|
||||
class JWTInvalidClaimFormat(JWException):
|
||||
"""JSON Web Token claim format is invalid.
|
||||
|
||||
This exception is raised when a claim is not in a valid format.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Invalid Claim Format'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTInvalidClaimFormat, self).__init__(msg)
|
||||
|
||||
|
||||
@deprecated('')
|
||||
class JWTMissingKeyID(JWException):
|
||||
"""JSON Web Token is missing key id.
|
||||
|
||||
This exception is raised when trying to decode a JWT with a key set
|
||||
that does not have a kid value in its header.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Missing Key ID'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTMissingKeyID, self).__init__(msg)
|
||||
|
||||
|
||||
class JWTMissingKey(JWKeyNotFound):
|
||||
"""JSON Web Token is using a key not in the key set.
|
||||
|
||||
This exception is raised if the key that was used is not available
|
||||
in the passed key set.
|
||||
"""
|
||||
|
||||
def __init__(self, message=None, exception=None):
|
||||
msg = None
|
||||
if message:
|
||||
msg = str(message)
|
||||
else:
|
||||
msg = 'Missing Key'
|
||||
if exception:
|
||||
msg += ' {%s}' % str(exception)
|
||||
super(JWTMissingKey, self).__init__(msg)
|
||||
|
||||
|
||||
class JWT:
|
||||
"""JSON Web token object
|
||||
|
||||
This object represent a generic token.
|
||||
"""
|
||||
|
||||
def __init__(self, header=None, claims=None, jwt=None, key=None,
|
||||
algs=None, default_claims=None, check_claims=None,
|
||||
expected_type=None):
|
||||
"""Creates a JWT object.
|
||||
|
||||
:param header: A dict or a JSON string with the JWT Header data.
|
||||
:param claims: A dict or a string with the JWT Claims data.
|
||||
:param jwt: a 'raw' JWT token
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) key to deserialize
|
||||
the token. A (:class:`jwcrypto.jwk.JWKSet`) can also be used.
|
||||
:param algs: An optional list of allowed algorithms
|
||||
:param default_claims: An optional dict with default values for
|
||||
registered claims. A None value for NumericDate type claims
|
||||
will cause generation according to system time. Only the values
|
||||
from RFC 7519 - 4.1 are evaluated.
|
||||
:param check_claims: An optional dict of claims that must be
|
||||
present in the token, if the value is not None the claim must
|
||||
match exactly.
|
||||
:param expected_type: An optional string that defines what kind
|
||||
of token to expect when validating a deserialized token.
|
||||
Supported values: "JWS" or "JWE"
|
||||
If left to None the code will try to detect what the expected
|
||||
type is based on other parameters like 'algs' and will default
|
||||
to JWS if no hints are found. It has no effect on token creation.
|
||||
|
||||
Note: either the header,claims or jwt,key parameters should be
|
||||
provided as a deserialization operation (which occurs if the jwt
|
||||
is provided) will wipe any header or claim provided by setting
|
||||
those obtained from the deserialization of the jwt token.
|
||||
|
||||
Note: if check_claims is not provided the 'exp' and 'nbf' claims
|
||||
are checked if they are set on the token but not enforced if not
|
||||
set. Any other RFC 7519 registered claims are checked only for
|
||||
format conformance.
|
||||
"""
|
||||
|
||||
self._header = None
|
||||
self._claims = None
|
||||
self._token = None
|
||||
self._algs = algs
|
||||
self._reg_claims = None
|
||||
self._check_claims = None
|
||||
self._leeway = 60 # 1 minute clock skew allowed
|
||||
self._validity = 600 # 10 minutes validity (up to 11 with leeway)
|
||||
self.deserializelog = None
|
||||
self._expected_type = expected_type
|
||||
|
||||
if header:
|
||||
self.header = header
|
||||
|
||||
if default_claims is not None:
|
||||
self._reg_claims = default_claims
|
||||
|
||||
if check_claims is not None:
|
||||
if check_claims is not False:
|
||||
self._check_check_claims(check_claims)
|
||||
self._check_claims = check_claims
|
||||
|
||||
if claims is not None:
|
||||
self.claims = claims
|
||||
|
||||
if jwt is not None:
|
||||
self.deserialize(jwt, key)
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
if self._header is None:
|
||||
raise KeyError("'header' not set")
|
||||
return self._header
|
||||
|
||||
@header.setter
|
||||
def header(self, h):
|
||||
if isinstance(h, dict):
|
||||
eh = json_encode(h)
|
||||
else:
|
||||
eh = h
|
||||
h = json_decode(eh)
|
||||
|
||||
if h.get('b64') is False:
|
||||
raise ValueError("b64 header is invalid."
|
||||
"JWTs cannot use unencoded payloads")
|
||||
self._header = eh
|
||||
|
||||
@property
|
||||
def claims(self):
|
||||
if self._claims is None:
|
||||
raise KeyError("'claims' not set")
|
||||
return self._claims
|
||||
|
||||
@claims.setter
|
||||
def claims(self, data):
|
||||
if not isinstance(data, dict):
|
||||
if not self._reg_claims:
|
||||
# no default_claims, can return immediately
|
||||
self._claims = data
|
||||
return
|
||||
data = json_decode(data)
|
||||
else:
|
||||
# _add_default_claims modifies its argument
|
||||
# so we must always copy it.
|
||||
data = copy.deepcopy(data)
|
||||
|
||||
self._add_default_claims(data)
|
||||
self._claims = json_encode(data)
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._token
|
||||
|
||||
@token.setter
|
||||
def token(self, t):
|
||||
if isinstance(t, JWS) or isinstance(t, JWE) or isinstance(t, JWT):
|
||||
self._token = t
|
||||
else:
|
||||
raise TypeError("Invalid token type, must be one of JWS,JWE,JWT")
|
||||
|
||||
@property
|
||||
def leeway(self):
|
||||
return self._leeway
|
||||
|
||||
@leeway.setter
|
||||
def leeway(self, lwy):
|
||||
self._leeway = int(lwy)
|
||||
|
||||
@property
|
||||
def validity(self):
|
||||
return self._validity
|
||||
|
||||
@validity.setter
|
||||
def validity(self, v):
|
||||
self._validity = int(v)
|
||||
|
||||
def _expected_type_heuristics(self, key=None):
|
||||
if self._expected_type is None and self._algs:
|
||||
if set(self._algs).issubset(jwe_algs + ['RSA1_5']):
|
||||
self._expected_type = "JWE"
|
||||
elif set(self._algs).issubset(jws_algs):
|
||||
self._expected_type = "JWS"
|
||||
if self._expected_type is None and self._header:
|
||||
if "enc" in json_decode(self._header):
|
||||
self._expected_type = "JWE"
|
||||
if self._expected_type is None and key is not None:
|
||||
if isinstance(key, JWK):
|
||||
use = key.get('use')
|
||||
if use == 'sig':
|
||||
self._expected_type = "JWS"
|
||||
elif use == 'enc':
|
||||
self._expected_type = "JWE"
|
||||
elif isinstance(key, JWKSet):
|
||||
all_use = None
|
||||
# we can infer only if all keys are of the same type
|
||||
for k in key:
|
||||
use = k.get('use')
|
||||
if all_use is None:
|
||||
all_use = use
|
||||
elif use != all_use:
|
||||
all_use = None
|
||||
break
|
||||
if all_use == 'sig':
|
||||
self._expected_type = "JWS"
|
||||
elif all_use == 'enc':
|
||||
self._expected_type = "JWE"
|
||||
if self._expected_type is None and key is not None:
|
||||
if isinstance(key, JWK):
|
||||
ops = key.get('key_ops')
|
||||
if ops:
|
||||
if not isinstance(ops, list):
|
||||
ops = [ops]
|
||||
if set(ops).issubset(['sign', 'verify']):
|
||||
self._expected_type = "JWS"
|
||||
elif set(ops).issubset(['encrypt', 'decrypt']):
|
||||
self._expected_type = "JWE"
|
||||
elif isinstance(key, JWKSet):
|
||||
all_ops = None
|
||||
ttype = None
|
||||
# we can infer only if all keys are of the same type
|
||||
for k in key:
|
||||
ops = k.get('key_ops')
|
||||
if ops:
|
||||
if not isinstance(ops, list):
|
||||
ops = [ops]
|
||||
if all_ops is None:
|
||||
if set(ops).issubset(['sign', 'verify']):
|
||||
all_ops = set(['sign', 'verify'])
|
||||
ttype = "JWS"
|
||||
elif set(ops).issubset(['encrypt', 'decrypt']):
|
||||
all_ops = set(['encrypt', 'decrypt'])
|
||||
ttype = "JWE"
|
||||
else:
|
||||
ttype = None
|
||||
break
|
||||
else:
|
||||
if not set(ops).issubset(all_ops):
|
||||
ttype = None
|
||||
break
|
||||
elif all_ops:
|
||||
ttype = None
|
||||
break
|
||||
if ttype:
|
||||
self._expected_type = ttype
|
||||
if self._expected_type is None:
|
||||
self._expected_type = "JWS"
|
||||
return self._expected_type
|
||||
|
||||
@property
|
||||
def expected_type(self):
|
||||
if self._expected_type is not None:
|
||||
return self._expected_type
|
||||
|
||||
# If no expected type is set we default to accept only JWSs,
|
||||
# however to improve backwards compatibility we try some
|
||||
# heuristic to see if there has been strong indication of
|
||||
# what the expected token type is.
|
||||
return self._expected_type_heuristics()
|
||||
|
||||
@expected_type.setter
|
||||
def expected_type(self, v):
|
||||
if v in ["JWS", "JWE"]:
|
||||
self._expected_type = v
|
||||
else:
|
||||
raise ValueError("Invalid value, must be 'JWS' or 'JWE'")
|
||||
|
||||
def _add_optional_claim(self, name, claims):
|
||||
if name in claims:
|
||||
return
|
||||
val = self._reg_claims.get(name, None)
|
||||
if val is not None:
|
||||
claims[name] = val
|
||||
|
||||
def _add_time_claim(self, name, claims, defval):
|
||||
if name in claims:
|
||||
return
|
||||
if name in self._reg_claims:
|
||||
if self._reg_claims[name] is None:
|
||||
claims[name] = defval
|
||||
else:
|
||||
claims[name] = self._reg_claims[name]
|
||||
|
||||
def _add_jti_claim(self, claims):
|
||||
if 'jti' in claims or 'jti' not in self._reg_claims:
|
||||
return
|
||||
claims['jti'] = str(uuid.uuid4())
|
||||
|
||||
def _add_default_claims(self, claims):
|
||||
if self._reg_claims is None:
|
||||
return
|
||||
|
||||
now = int(time.time())
|
||||
self._add_optional_claim('iss', claims)
|
||||
self._add_optional_claim('sub', claims)
|
||||
self._add_optional_claim('aud', claims)
|
||||
self._add_time_claim('exp', claims, now + self.validity)
|
||||
self._add_time_claim('nbf', claims, now)
|
||||
self._add_time_claim('iat', claims, now)
|
||||
self._add_jti_claim(claims)
|
||||
|
||||
def _check_string_claim(self, name, claims):
|
||||
if name not in claims or claims[name] is None:
|
||||
return
|
||||
if not isinstance(claims[name], str):
|
||||
raise JWTInvalidClaimFormat(
|
||||
"Claim %s is not a StringOrURI type" % (name, ))
|
||||
|
||||
def _check_array_or_string_claim(self, name, claims):
|
||||
if name not in claims or claims[name] is None:
|
||||
return
|
||||
if isinstance(claims[name], list):
|
||||
if any(not isinstance(claim, str) for claim in claims):
|
||||
raise JWTInvalidClaimFormat(
|
||||
"Claim %s contains non StringOrURI types" % (name, ))
|
||||
elif not isinstance(claims[name], str):
|
||||
raise JWTInvalidClaimFormat(
|
||||
"Claim %s is not a StringOrURI type" % (name, ))
|
||||
|
||||
def _check_integer_claim(self, name, claims):
|
||||
if name not in claims or claims[name] is None:
|
||||
return
|
||||
try:
|
||||
int(claims[name])
|
||||
except ValueError as e:
|
||||
raise JWTInvalidClaimFormat(
|
||||
"Claim %s is not an integer" % (name, )) from e
|
||||
|
||||
def _check_exp(self, claim, limit, leeway):
|
||||
if claim < limit - leeway:
|
||||
raise JWTExpired('Expired at %d, time: %d(leeway: %d)' % (
|
||||
claim, limit, leeway))
|
||||
|
||||
def _check_nbf(self, claim, limit, leeway):
|
||||
if claim > limit + leeway:
|
||||
raise JWTNotYetValid('Valid from %d, time: %d(leeway: %d)' % (
|
||||
claim, limit, leeway))
|
||||
|
||||
def _check_default_claims(self, claims):
|
||||
self._check_string_claim('iss', claims)
|
||||
self._check_string_claim('sub', claims)
|
||||
self._check_array_or_string_claim('aud', claims)
|
||||
self._check_integer_claim('exp', claims)
|
||||
self._check_integer_claim('nbf', claims)
|
||||
self._check_integer_claim('iat', claims)
|
||||
self._check_string_claim('jti', claims)
|
||||
self._check_string_claim('typ', claims)
|
||||
|
||||
if self._check_claims is None:
|
||||
if 'exp' in claims:
|
||||
self._check_exp(claims['exp'], time.time(), self._leeway)
|
||||
if 'nbf' in claims:
|
||||
self._check_nbf(claims['nbf'], time.time(), self._leeway)
|
||||
|
||||
def _check_check_claims(self, check_claims):
|
||||
self._check_string_claim('iss', check_claims)
|
||||
self._check_string_claim('sub', check_claims)
|
||||
self._check_array_or_string_claim('aud', check_claims)
|
||||
self._check_integer_claim('exp', check_claims)
|
||||
self._check_integer_claim('nbf', check_claims)
|
||||
self._check_integer_claim('iat', check_claims)
|
||||
self._check_string_claim('jti', check_claims)
|
||||
self._check_string_claim('typ', check_claims)
|
||||
|
||||
def _check_provided_claims(self):
|
||||
# check_claims can be set to False to skip any check
|
||||
if self._check_claims is False:
|
||||
return
|
||||
|
||||
try:
|
||||
claims = json_decode(self.claims)
|
||||
if not isinstance(claims, dict):
|
||||
raise ValueError()
|
||||
except ValueError as e:
|
||||
if self._check_claims is not None:
|
||||
raise JWTInvalidClaimFormat("Claims check requested "
|
||||
"but claims is not a json "
|
||||
"dict") from e
|
||||
return
|
||||
|
||||
self._check_default_claims(claims)
|
||||
|
||||
if self._check_claims is None:
|
||||
return
|
||||
|
||||
for name, value in self._check_claims.items():
|
||||
if name not in claims:
|
||||
raise JWTMissingClaim("Claim %s is missing" % (name, ))
|
||||
|
||||
if name in ['iss', 'sub', 'jti']:
|
||||
if value is not None and value != claims[name]:
|
||||
raise JWTInvalidClaimValue(
|
||||
"Invalid '%s' value. Expected '%s' got '%s'" % (
|
||||
name, value, claims[name]))
|
||||
|
||||
elif name == 'aud':
|
||||
if value is not None:
|
||||
if isinstance(claims[name], list):
|
||||
tclaims = claims[name]
|
||||
else:
|
||||
tclaims = [claims[name]]
|
||||
if isinstance(value, list):
|
||||
cclaims = value
|
||||
else:
|
||||
cclaims = [value]
|
||||
found = False
|
||||
for v in cclaims:
|
||||
if v in tclaims:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise JWTInvalidClaimValue(
|
||||
"Invalid '{}' value. Expected '{}' in '{}'".format(
|
||||
name, claims[name], value))
|
||||
|
||||
elif name == 'exp':
|
||||
if value is not None:
|
||||
self._check_exp(claims[name], value, 0)
|
||||
else:
|
||||
self._check_exp(claims[name], time.time(), self._leeway)
|
||||
|
||||
elif name == 'nbf':
|
||||
if value is not None:
|
||||
self._check_nbf(claims[name], value, 0)
|
||||
else:
|
||||
self._check_nbf(claims[name], time.time(), self._leeway)
|
||||
|
||||
elif name == 'typ':
|
||||
if value is not None:
|
||||
if self.norm_typ(value) != self.norm_typ(claims[name]):
|
||||
raise JWTInvalidClaimValue("Invalid '%s' value. '%s'"
|
||||
" does not normalize to "
|
||||
"'%s'" % (name,
|
||||
claims[name],
|
||||
value))
|
||||
|
||||
else:
|
||||
if value is not None and value != claims[name]:
|
||||
raise JWTInvalidClaimValue(
|
||||
"Invalid '%s' value. Expected '%s' got '%s'" % (
|
||||
name, value, claims[name]))
|
||||
|
||||
def norm_typ(self, val):
|
||||
lc = val.lower()
|
||||
if '/' in lc:
|
||||
return lc
|
||||
else:
|
||||
return 'application/' + lc
|
||||
|
||||
def make_signed_token(self, key):
|
||||
"""Signs the payload.
|
||||
|
||||
Creates a JWS token with the header as the JWS protected header and
|
||||
the claims as the payload. See (:class:`jwcrypto.jws.JWS`) for
|
||||
details on the exceptions that may be raised.
|
||||
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) key.
|
||||
"""
|
||||
|
||||
t = JWS(self.claims)
|
||||
if self._algs:
|
||||
t.allowed_algs = self._algs
|
||||
t.add_signature(key, protected=self.header)
|
||||
self.token = t
|
||||
self._expected_type = "JWS"
|
||||
|
||||
def make_encrypted_token(self, key):
|
||||
"""Encrypts the payload.
|
||||
|
||||
Creates a JWE token with the header as the JWE protected header and
|
||||
the claims as the plaintext. See (:class:`jwcrypto.jwe.JWE`) for
|
||||
details on the exceptions that may be raised.
|
||||
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) key.
|
||||
"""
|
||||
|
||||
t = JWE(self.claims, self.header)
|
||||
if self._algs:
|
||||
t.allowed_algs = self._algs
|
||||
t.add_recipient(key)
|
||||
self.token = t
|
||||
self._expected_type = "JWE"
|
||||
|
||||
def validate(self, key):
|
||||
"""Validate a JWT token that was deserialized w/o providing a key
|
||||
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
|
||||
decryption key, or a (:class:`jwcrypto.jwk.JWKSet`) that
|
||||
contains a key indexed by the 'kid' header.
|
||||
"""
|
||||
self.deserializelog = []
|
||||
if self.token is None:
|
||||
raise ValueError("Token empty")
|
||||
|
||||
et = self._expected_type_heuristics(key)
|
||||
validate_fn = None
|
||||
|
||||
if isinstance(self.token, JWS):
|
||||
if et != "JWS" and JWT_expect_type:
|
||||
raise TypeError("Expected {}, got JWS".format(et))
|
||||
validate_fn = self.token.verify
|
||||
elif isinstance(self.token, JWE):
|
||||
if et != "JWE" and JWT_expect_type:
|
||||
raise TypeError("Expected {}, got JWE".format(et))
|
||||
validate_fn = self.token.decrypt
|
||||
else:
|
||||
raise ValueError("Token format unrecognized")
|
||||
|
||||
try:
|
||||
validate_fn(key)
|
||||
self.deserializelog.append("Success")
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
if isinstance(self.token, JWS):
|
||||
self.deserializelog = self.token.verifylog
|
||||
elif isinstance(self.token, JWE):
|
||||
self.deserializelog = self.token.decryptlog
|
||||
self.deserializelog.append(
|
||||
'Validation failed: [{}]'.format(repr(e)))
|
||||
if isinstance(e, JWKeyNotFound):
|
||||
raise JWTMissingKey() from e
|
||||
raise
|
||||
|
||||
self.header = self.token.jose_header
|
||||
payload = self.token.payload
|
||||
if isinstance(payload, bytes):
|
||||
payload = payload.decode('utf-8')
|
||||
self.claims = payload
|
||||
self._check_provided_claims()
|
||||
|
||||
def deserialize(self, jwt, key=None):
|
||||
"""Deserialize a JWT token.
|
||||
|
||||
NOTE: Destroys any current status and tries to import the raw
|
||||
token provided.
|
||||
|
||||
:param jwt: a 'raw' JWT token.
|
||||
:param key: A (:class:`jwcrypto.jwk.JWK`) verification or
|
||||
decryption key, or a (:class:`jwcrypto.jwk.JWKSet`) that
|
||||
contains a key indexed by the 'kid' header.
|
||||
"""
|
||||
data = jwt.count('.')
|
||||
if data == 2:
|
||||
self.token = JWS()
|
||||
elif data == 4:
|
||||
self.token = JWE()
|
||||
else:
|
||||
raise ValueError("Token format unrecognized")
|
||||
|
||||
# Apply algs restrictions if any, before performing any operation
|
||||
if self._algs:
|
||||
self.token.allowed_algs = self._algs
|
||||
|
||||
self.deserializelog = None
|
||||
# now deserialize and also decrypt/verify (or raise) if we
|
||||
# have a key
|
||||
self.token.deserialize(jwt, None)
|
||||
if key:
|
||||
self.validate(key)
|
||||
|
||||
def serialize(self, compact=True):
|
||||
"""Serializes the object into a JWS token.
|
||||
|
||||
:param compact(boolean): must be True.
|
||||
|
||||
Note: the compact parameter is provided for general compatibility
|
||||
with the serialize() functions of :class:`jwcrypto.jws.JWS` and
|
||||
:class:`jwcrypto.jwe.JWE` so that these objects can all be used
|
||||
interchangeably. However the only valid JWT representation is the
|
||||
compact representation.
|
||||
|
||||
:return: A json formatted string or a compact representation string
|
||||
:rtype: `str`
|
||||
"""
|
||||
if not compact:
|
||||
raise ValueError("Only the compact serialization is allowed")
|
||||
|
||||
return self.token.serialize(compact)
|
||||
|
||||
@classmethod
|
||||
def from_jose_token(cls, token):
|
||||
"""Creates a JWT object from a serialized JWT token.
|
||||
|
||||
:param token: A string with the json or compat representation
|
||||
of the token.
|
||||
|
||||
:raises InvalidJWEData or InvalidJWSObject: if the raw object is an
|
||||
invalid JWT token.
|
||||
|
||||
:return: A JWT token
|
||||
:rtype: JWT
|
||||
"""
|
||||
|
||||
obj = cls()
|
||||
obj.deserialize(token)
|
||||
return obj
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JWT):
|
||||
return False
|
||||
return self._claims == other._claims and \
|
||||
self._header == other._header and \
|
||||
self.token == other.token
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
return self.serialize()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.__repr__()
|
||||
|
||||
def __repr__(self):
|
||||
jwt = repr(self.token)
|
||||
return f'JWT(header={self._header}, ' + \
|
||||
f'claims={self._claims}, ' + \
|
||||
f'jwt={jwt}, ' + \
|
||||
f'key=None, algs={self._algs}, ' + \
|
||||
f'default_claims={self._reg_claims}, ' + \
|
||||
f'check_claims={self._check_claims})'
|
||||
1294
venv/lib/python3.11/site-packages/jwcrypto/tests-cookbook.py
Normal file
1294
venv/lib/python3.11/site-packages/jwcrypto/tests-cookbook.py
Normal file
File diff suppressed because it is too large
Load Diff
2388
venv/lib/python3.11/site-packages/jwcrypto/tests.py
Normal file
2388
venv/lib/python3.11/site-packages/jwcrypto/tests.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user