mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 17:51:08 -05:00
okay fine
This commit is contained in:
206
.venv/lib/python3.12/site-packages/jwcrypto/common.py
Normal file
206
.venv/lib/python3.12/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.12/site-packages/jwcrypto/jwa.py
Normal file
1202
.venv/lib/python3.12/site-packages/jwcrypto/jwa.py
Normal file
File diff suppressed because it is too large
Load Diff
615
.venv/lib/python3.12/site-packages/jwcrypto/jwe.py
Normal file
615
.venv/lib/python3.12/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.12/site-packages/jwcrypto/jwk.py
Normal file
1414
.venv/lib/python3.12/site-packages/jwcrypto/jwk.py
Normal file
File diff suppressed because it is too large
Load Diff
721
.venv/lib/python3.12/site-packages/jwcrypto/jws.py
Normal file
721
.venv/lib/python3.12/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.12/site-packages/jwcrypto/jwt.py
Normal file
740
.venv/lib/python3.12/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.12/site-packages/jwcrypto/tests-cookbook.py
Normal file
1294
.venv/lib/python3.12/site-packages/jwcrypto/tests-cookbook.py
Normal file
File diff suppressed because it is too large
Load Diff
2388
.venv/lib/python3.12/site-packages/jwcrypto/tests.py
Normal file
2388
.venv/lib/python3.12/site-packages/jwcrypto/tests.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user