initial commit
This commit is contained in:
51
billinglayer/python/jwt/__init__.py
Normal file
51
billinglayer/python/jwt/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .jwa import std_hash_by_alg
|
||||
from .jwk import (
|
||||
AbstractJWKBase,
|
||||
jwk_from_dict,
|
||||
jwk_from_bytes,
|
||||
jwk_from_pem,
|
||||
jwk_from_der,
|
||||
supported_key_types,
|
||||
)
|
||||
from .jwkset import JWKSet
|
||||
from .jwa import (
|
||||
AbstractSigningAlgorithm,
|
||||
supported_signing_algorithms,
|
||||
)
|
||||
from .jwt import JWT
|
||||
|
||||
|
||||
__all__ = [
|
||||
# .jwa
|
||||
'std_hash_by_alg',
|
||||
# .jwk
|
||||
'AbstractJWKBase',
|
||||
'jwk_from_bytes',
|
||||
'jwk_from_dict',
|
||||
'jwk_from_pem',
|
||||
'jwk_from_der',
|
||||
'supported_key_types',
|
||||
# .jwkset
|
||||
'JWKSet',
|
||||
# .jws
|
||||
'AbstractSigningAlgorithm',
|
||||
'supported_signing_algorithms',
|
||||
# .jwt
|
||||
'JWT',
|
||||
]
|
||||
BIN
billinglayer/python/jwt/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/exceptions.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/exceptions.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/jwa.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/jwa.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/jwk.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/jwk.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/jwkset.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/jwkset.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/jws.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/jws.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/jwt.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/jwt.cpython-311.pyc
Normal file
Binary file not shown.
BIN
billinglayer/python/jwt/__pycache__/utils.cpython-311.pyc
Normal file
BIN
billinglayer/python/jwt/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
49
billinglayer/python/jwt/exceptions.py
Normal file
49
billinglayer/python/jwt/exceptions.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class JWTException(Exception):
|
||||
"""
|
||||
common base class for all exceptions used in python-jwt
|
||||
"""
|
||||
|
||||
|
||||
class MalformedJWKError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedKeyTypeError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeyTypeError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class JWSEncodeError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class JWSDecodeError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class JWTEncodeError(JWTException):
|
||||
pass
|
||||
|
||||
|
||||
class JWTDecodeError(JWTException):
|
||||
pass
|
||||
198
billinglayer/python/jwt/jwa.py
Normal file
198
billinglayer/python/jwt/jwa.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Callable,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.hashes import (
|
||||
SHA256,
|
||||
SHA384,
|
||||
SHA512,
|
||||
)
|
||||
|
||||
from .exceptions import InvalidKeyTypeError
|
||||
from .jwk import AbstractJWKBase
|
||||
|
||||
|
||||
def std_hash_by_alg(alg: str) -> Callable[[bytes], object]:
|
||||
if alg.endswith('S256'):
|
||||
return hashlib.sha256
|
||||
if alg.endswith('S384'):
|
||||
return hashlib.sha384
|
||||
if alg.endswith('S512'):
|
||||
return hashlib.sha512
|
||||
raise ValueError('{} is not supported'.format(alg))
|
||||
|
||||
|
||||
class AbstractSigningAlgorithm:
|
||||
|
||||
def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes:
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
def verify(self, message: bytes, key: Optional[AbstractJWKBase],
|
||||
signature: bytes) -> bool:
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
|
||||
class NoneAlgorithm(AbstractSigningAlgorithm):
|
||||
|
||||
def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes:
|
||||
return b''
|
||||
|
||||
def verify(self, message: bytes, key: Optional[AbstractJWKBase],
|
||||
signature: bytes) -> bool:
|
||||
return hmac.compare_digest(signature, b'')
|
||||
|
||||
|
||||
none = NoneAlgorithm()
|
||||
|
||||
|
||||
class HMACAlgorithm(AbstractSigningAlgorithm):
|
||||
|
||||
def __init__(self, hash_fun: Callable[[], object]) -> None:
|
||||
self.hash_fun = hash_fun
|
||||
|
||||
def _check_key(self, key: Optional[AbstractJWKBase]) -> AbstractJWKBase:
|
||||
if not key or key.get_kty() != 'oct':
|
||||
raise InvalidKeyTypeError('Octet key is required')
|
||||
return key
|
||||
|
||||
def _sign(self, message: bytes, key: bytes) -> bytes:
|
||||
return hmac.new(key, message, self.hash_fun).digest()
|
||||
|
||||
def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes:
|
||||
key = self._check_key(key)
|
||||
return key.sign(message, signer=self._sign)
|
||||
|
||||
def verify(self, message: bytes, key: Optional[AbstractJWKBase],
|
||||
signature: bytes) -> bool:
|
||||
key = self._check_key(key)
|
||||
return key.verify(message, signature, signer=self._sign)
|
||||
|
||||
|
||||
HS256 = HMACAlgorithm(hashlib.sha256)
|
||||
HS384 = HMACAlgorithm(hashlib.sha384)
|
||||
HS512 = HMACAlgorithm(hashlib.sha512)
|
||||
|
||||
|
||||
class RSAAlgorithm(AbstractSigningAlgorithm):
|
||||
|
||||
def __init__(self, hash_fun: object) -> None:
|
||||
self.hash_fun = hash_fun
|
||||
|
||||
def _check_key(
|
||||
self,
|
||||
key: Optional[AbstractJWKBase],
|
||||
must_sign_key: bool = False,
|
||||
) -> AbstractJWKBase:
|
||||
if not key or key.get_kty() != 'RSA':
|
||||
raise InvalidKeyTypeError('RSA key is required')
|
||||
if must_sign_key and not key.is_sign_key():
|
||||
raise InvalidKeyTypeError(
|
||||
'a RSA private key is required, but passed is RSA public key')
|
||||
return key
|
||||
|
||||
def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes:
|
||||
key = self._check_key(key, must_sign_key=True)
|
||||
return key.sign(message, hash_fun=self.hash_fun,
|
||||
padding=padding.PKCS1v15())
|
||||
|
||||
def verify(
|
||||
self,
|
||||
message: bytes,
|
||||
key: Optional[AbstractJWKBase],
|
||||
signature: bytes,
|
||||
) -> bool:
|
||||
key = self._check_key(key)
|
||||
return key.verify(message, signature, hash_fun=self.hash_fun,
|
||||
padding=padding.PKCS1v15())
|
||||
|
||||
|
||||
RS256 = RSAAlgorithm(SHA256)
|
||||
RS384 = RSAAlgorithm(SHA384)
|
||||
RS512 = RSAAlgorithm(SHA512)
|
||||
|
||||
|
||||
class PSSRSAAlgorithm(AbstractSigningAlgorithm):
|
||||
def __init__(self, hash_fun: Callable[[], Any]) -> None:
|
||||
self.hash_fun = hash_fun
|
||||
|
||||
def _check_key(
|
||||
self,
|
||||
key: Optional[AbstractJWKBase],
|
||||
must_sign_key: bool = False,
|
||||
) -> AbstractJWKBase:
|
||||
if not key or key.get_kty() != 'RSA':
|
||||
raise InvalidKeyTypeError('RSA key is required')
|
||||
if must_sign_key and not key.is_sign_key():
|
||||
raise InvalidKeyTypeError(
|
||||
'a RSA private key is required, but passed is RSA public key')
|
||||
return key
|
||||
|
||||
def sign(self, message: bytes, key: Optional[AbstractJWKBase]) -> bytes:
|
||||
key = self._check_key(key, must_sign_key=True)
|
||||
return key.sign(
|
||||
message,
|
||||
hash_fun=self.hash_fun,
|
||||
padding=padding.PSS( # type: ignore[no-untyped-call]
|
||||
mgf=padding.MGF1(self.hash_fun()),
|
||||
salt_length=self.hash_fun().digest_size,
|
||||
),
|
||||
)
|
||||
|
||||
def verify(
|
||||
self,
|
||||
message: bytes,
|
||||
key: Optional[AbstractJWKBase],
|
||||
signature: bytes
|
||||
) -> bool:
|
||||
key = self._check_key(key)
|
||||
return key.verify(
|
||||
message,
|
||||
signature,
|
||||
hash_fun=self.hash_fun,
|
||||
padding=padding.PSS( # type: ignore[no-untyped-call]
|
||||
mgf=padding.MGF1(self.hash_fun()),
|
||||
salt_length=self.hash_fun().digest_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
PS256 = PSSRSAAlgorithm(SHA256)
|
||||
PS384 = PSSRSAAlgorithm(SHA384)
|
||||
PS512 = PSSRSAAlgorithm(SHA512)
|
||||
|
||||
|
||||
def supported_signing_algorithms() -> Dict[str, AbstractSigningAlgorithm]:
|
||||
# NOTE(yosida95): exclude vulnerable 'none' algorithm by default.
|
||||
return {
|
||||
'HS256': HS256,
|
||||
'HS384': HS384,
|
||||
'HS512': HS512,
|
||||
'RS256': RS256,
|
||||
'RS384': RS384,
|
||||
'RS512': RS512,
|
||||
'PS256': PS256,
|
||||
'PS384': PS384,
|
||||
'PS512': PS512,
|
||||
}
|
||||
427
billinglayer/python/jwt/jwk.py
Normal file
427
billinglayer/python/jwt/jwk.py
Normal file
@@ -0,0 +1,427 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import hmac
|
||||
from warnings import warn
|
||||
from abc import (
|
||||
ABC,
|
||||
abstractmethod,
|
||||
)
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Mapping,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
Optional
|
||||
)
|
||||
from functools import wraps
|
||||
|
||||
import cryptography.hazmat.primitives.serialization as serialization_module
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
rsa_crt_dmp1,
|
||||
rsa_crt_dmq1,
|
||||
rsa_crt_iqmp,
|
||||
rsa_recover_prime_factors,
|
||||
RSAPrivateKey,
|
||||
RSAPrivateNumbers,
|
||||
RSAPublicKey,
|
||||
RSAPublicNumbers,
|
||||
)
|
||||
from cryptography.hazmat.primitives.hashes import HashAlgorithm
|
||||
|
||||
from .exceptions import (
|
||||
MalformedJWKError,
|
||||
UnsupportedKeyTypeError,
|
||||
)
|
||||
from .utils import (
|
||||
b64encode,
|
||||
b64decode,
|
||||
uint_b64encode,
|
||||
uint_b64decode,
|
||||
)
|
||||
|
||||
_AJWK = TypeVar("_AJWK", bound="AbstractJWKBase")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class AbstractJWKBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get_kty(self) -> str:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def get_kid(self) -> str:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def is_sign_key(self) -> bool:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def sign(self, message: bytes, **options) -> bytes:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, message: bytes, signature: bytes, **options) -> bool:
|
||||
pass # pragma: no cover
|
||||
|
||||
@abstractmethod
|
||||
def to_dict(self, public_only: bool = True) -> Dict[str, str]:
|
||||
pass # pragma: no cover
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def from_dict(cls: Type[_AJWK], dct: Dict[str, object]) -> _AJWK:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class OctetJWK(AbstractJWKBase):
|
||||
|
||||
def __init__(self, key: bytes, kid=None, **options) -> None:
|
||||
super(AbstractJWKBase, self).__init__()
|
||||
self.key = key
|
||||
self.kid = kid
|
||||
|
||||
optnames = {'use', 'key_ops', 'alg', 'x5u', 'x5c', 'x5t', 'x5t#s256'}
|
||||
self.options = {k: v for k, v in options.items() if k in optnames}
|
||||
|
||||
def get_kty(self):
|
||||
return 'oct'
|
||||
|
||||
def get_kid(self):
|
||||
return self.kid
|
||||
|
||||
def is_sign_key(self) -> bool:
|
||||
return True
|
||||
|
||||
def _get_signer(self, options) -> Callable[[bytes, bytes], bytes]:
|
||||
return options['signer']
|
||||
|
||||
def sign(self, message: bytes, **options) -> bytes:
|
||||
signer = self._get_signer(options)
|
||||
return signer(message, self.key)
|
||||
|
||||
def verify(self, message: bytes, signature: bytes, **options) -> bool:
|
||||
signer = self._get_signer(options)
|
||||
return hmac.compare_digest(signature, signer(message, self.key))
|
||||
|
||||
def to_dict(self, public_only=True):
|
||||
dct = {
|
||||
'kty': 'oct',
|
||||
'k': b64encode(self.key),
|
||||
}
|
||||
dct.update(self.options)
|
||||
if self.kid:
|
||||
dct['kid'] = self.kid
|
||||
return dct
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dct):
|
||||
try:
|
||||
return cls(b64decode(dct['k']), **dct)
|
||||
except KeyError as why:
|
||||
raise MalformedJWKError('k is required') from why
|
||||
|
||||
|
||||
class RSAJWK(AbstractJWKBase):
|
||||
"""
|
||||
https://tools.ietf.org/html/rfc7518.html#section-6.3.1
|
||||
"""
|
||||
|
||||
def __init__(self, keyobj: Union[RSAPrivateKey, RSAPublicKey],
|
||||
**options) -> None:
|
||||
super(AbstractJWKBase, self).__init__()
|
||||
self.keyobj = keyobj
|
||||
|
||||
optnames = {'use', 'key_ops', 'alg', 'kid',
|
||||
'x5u', 'x5c', 'x5t', 'x5t#s256', }
|
||||
self.options = {k: v for k, v in options.items() if k in optnames}
|
||||
|
||||
def is_sign_key(self) -> bool:
|
||||
return isinstance(self.keyobj, RSAPrivateKey)
|
||||
|
||||
def _get_hash_fun(self, options) -> Callable[[], HashAlgorithm]:
|
||||
return options['hash_fun']
|
||||
|
||||
def _get_padding(self, options) -> padding.AsymmetricPadding:
|
||||
try:
|
||||
return options['padding']
|
||||
except KeyError:
|
||||
warn('you should not use RSAJWK.verify/sign without jwa '
|
||||
'intermiediary, used legacy padding')
|
||||
return padding.PKCS1v15()
|
||||
|
||||
def sign(self, message: bytes, **options) -> bytes:
|
||||
if isinstance(self.keyobj, RSAPublicKey):
|
||||
raise ValueError("Requires a private key.")
|
||||
hash_fun = self._get_hash_fun(options)
|
||||
_padding = self._get_padding(options)
|
||||
return self.keyobj.sign(message, _padding, hash_fun())
|
||||
|
||||
def verify(self, message: bytes, signature: bytes, **options) -> bool:
|
||||
hash_fun = self._get_hash_fun(options)
|
||||
_padding = self._get_padding(options)
|
||||
if isinstance(self.keyobj, RSAPrivateKey):
|
||||
pubkey = self.keyobj.public_key()
|
||||
else:
|
||||
pubkey = self.keyobj
|
||||
try:
|
||||
pubkey.verify(signature, message, _padding, hash_fun())
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
def get_kty(self):
|
||||
return 'RSA'
|
||||
|
||||
def get_kid(self):
|
||||
return self.options.get('kid')
|
||||
|
||||
def to_dict(self, public_only=True):
|
||||
dct = {
|
||||
'kty': 'RSA',
|
||||
}
|
||||
dct.update(self.options)
|
||||
|
||||
if isinstance(self.keyobj, RSAPrivateKey):
|
||||
priv_numbers = self.keyobj.private_numbers()
|
||||
pub_numbers = priv_numbers.public_numbers
|
||||
dct.update({
|
||||
'e': uint_b64encode(pub_numbers.e),
|
||||
'n': uint_b64encode(pub_numbers.n),
|
||||
})
|
||||
if not public_only:
|
||||
dct.update({
|
||||
'e': uint_b64encode(pub_numbers.e),
|
||||
'n': uint_b64encode(pub_numbers.n),
|
||||
'd': uint_b64encode(priv_numbers.d),
|
||||
'p': uint_b64encode(priv_numbers.p),
|
||||
'q': uint_b64encode(priv_numbers.q),
|
||||
'dp': uint_b64encode(priv_numbers.dmp1),
|
||||
'dq': uint_b64encode(priv_numbers.dmq1),
|
||||
'qi': uint_b64encode(priv_numbers.iqmp),
|
||||
})
|
||||
return dct
|
||||
pub_numbers = self.keyobj.public_numbers()
|
||||
dct.update({
|
||||
'e': uint_b64encode(pub_numbers.e),
|
||||
'n': uint_b64encode(pub_numbers.n),
|
||||
})
|
||||
return dct
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dct):
|
||||
if 'oth' in dct:
|
||||
raise UnsupportedKeyTypeError(
|
||||
'RSA keys with multiples primes are not supported')
|
||||
|
||||
try:
|
||||
e = uint_b64decode(dct['e'])
|
||||
n = uint_b64decode(dct['n'])
|
||||
except KeyError as why:
|
||||
raise MalformedJWKError('e and n are required') from why
|
||||
pub_numbers = RSAPublicNumbers(e, n)
|
||||
if 'd' not in dct:
|
||||
return cls(
|
||||
pub_numbers.public_key(backend=default_backend()), **dct)
|
||||
d = uint_b64decode(dct['d'])
|
||||
|
||||
privparams = {'p', 'q', 'dp', 'dq', 'qi'}
|
||||
product = set(dct.keys()) & privparams
|
||||
if len(product) == 0:
|
||||
p, q = rsa_recover_prime_factors(n, e, d)
|
||||
priv_numbers = RSAPrivateNumbers(
|
||||
d=d,
|
||||
p=p,
|
||||
q=q,
|
||||
dmp1=rsa_crt_dmp1(d, p),
|
||||
dmq1=rsa_crt_dmq1(d, q),
|
||||
iqmp=rsa_crt_iqmp(p, q),
|
||||
public_numbers=pub_numbers)
|
||||
elif product == privparams:
|
||||
priv_numbers = RSAPrivateNumbers(
|
||||
d=d,
|
||||
p=uint_b64decode(dct['p']),
|
||||
q=uint_b64decode(dct['q']),
|
||||
dmp1=uint_b64decode(dct['dp']),
|
||||
dmq1=uint_b64decode(dct['dq']),
|
||||
iqmp=uint_b64decode(dct['qi']),
|
||||
public_numbers=pub_numbers)
|
||||
else:
|
||||
# If the producer includes any of the other private key parameters,
|
||||
# then all of the others MUST be present, with the exception of
|
||||
# "oth", which MUST only be present when more than two prime
|
||||
# factors were used.
|
||||
raise MalformedJWKError(
|
||||
'p, q, dp, dq, qi MUST be present or'
|
||||
'all of them MUST be absent')
|
||||
return cls(priv_numbers.private_key(backend=default_backend()), **dct)
|
||||
|
||||
|
||||
def supported_key_types() -> Dict[str, Type[AbstractJWKBase]]:
|
||||
return {
|
||||
'oct': OctetJWK,
|
||||
'RSA': RSAJWK,
|
||||
}
|
||||
|
||||
|
||||
def jwk_from_dict(dct: Mapping[str, Any]) -> AbstractJWKBase:
|
||||
if not isinstance(dct, dict): # pragma: no cover
|
||||
raise TypeError('dct must be a dict')
|
||||
if 'kty' not in dct:
|
||||
raise MalformedJWKError('kty MUST be present')
|
||||
|
||||
supported = supported_key_types()
|
||||
kty = dct['kty']
|
||||
if kty not in supported:
|
||||
raise UnsupportedKeyTypeError('unsupported key type: {}'.format(kty))
|
||||
return supported[kty].from_dict(dct)
|
||||
|
||||
|
||||
PublicKeyLoaderT = Union[str, Callable[[bytes, object], object]]
|
||||
PrivateKeyLoaderT = Union[
|
||||
str,
|
||||
Callable[[bytes, Optional[str], object], object]]
|
||||
_Loader = TypeVar("_Loader", PublicKeyLoaderT, PrivateKeyLoaderT)
|
||||
_C = TypeVar("_C", bound=Callable[..., Any])
|
||||
|
||||
|
||||
# The above LoaderTs should actually not be Union, and this function should be
|
||||
# typed something like this. But, this will lose any kwargs from the typing
|
||||
# information. Probably needs: https://github.com/python/mypy/issues/3157
|
||||
# (func: Callable[[bytes, _Loader], _T])
|
||||
# -> Callable[[bytes, Union[str, _Loader]], _T]
|
||||
def jwk_from_bytes_argument_conversion(func: _C) -> _C:
|
||||
if not ('private' in func.__name__ or 'public' in func.__name__):
|
||||
raise Exception("the wrapped function must have either public"
|
||||
" or private in it's name")
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(content, loader, **kwargs):
|
||||
# now convert it to a Callable if it's a string
|
||||
if isinstance(loader, str):
|
||||
loader = getattr(serialization_module, loader)
|
||||
|
||||
if kwargs.get('options') is None:
|
||||
kwargs['options'] = {}
|
||||
|
||||
return func(content, loader, **kwargs)
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
@jwk_from_bytes_argument_conversion
|
||||
def jwk_from_private_bytes(
|
||||
content: bytes,
|
||||
private_loader: PrivateKeyLoaderT,
|
||||
*,
|
||||
password: Optional[str] = None,
|
||||
backend: Optional[object] = None,
|
||||
options: Optional[Mapping[str, object]] = None,
|
||||
) -> AbstractJWKBase:
|
||||
"""This function is meant to be called from jwk_from_bytes"""
|
||||
if options is None:
|
||||
options = {}
|
||||
try:
|
||||
privkey = private_loader(content, password, backend) # type: ignore[operator] # noqa: E501
|
||||
if isinstance(privkey, RSAPrivateKey):
|
||||
return RSAJWK(privkey, **options)
|
||||
raise UnsupportedKeyTypeError('unsupported key type')
|
||||
except ValueError as ex:
|
||||
raise UnsupportedKeyTypeError('this is probably a public key') from ex
|
||||
|
||||
|
||||
@jwk_from_bytes_argument_conversion
|
||||
def jwk_from_public_bytes(
|
||||
content: bytes,
|
||||
public_loader: PublicKeyLoaderT,
|
||||
*,
|
||||
backend: Optional[object] = None,
|
||||
options: Optional[Mapping[str, object]] = None
|
||||
) -> AbstractJWKBase:
|
||||
"""This function is meant to be called from jwk_from_bytes"""
|
||||
if options is None:
|
||||
options = {}
|
||||
try:
|
||||
pubkey = public_loader(content, backend) # type: ignore[operator]
|
||||
if isinstance(pubkey, RSAPublicKey):
|
||||
return RSAJWK(pubkey, **options)
|
||||
raise UnsupportedKeyTypeError(
|
||||
'unsupported key type') # pragma: no cover
|
||||
except ValueError as why:
|
||||
raise UnsupportedKeyTypeError('could not deserialize') from why
|
||||
|
||||
|
||||
def jwk_from_bytes(
|
||||
content: bytes,
|
||||
private_loader: PrivateKeyLoaderT,
|
||||
public_loader: PublicKeyLoaderT,
|
||||
*,
|
||||
private_password: Optional[str] = None,
|
||||
backend: Optional[object] = None,
|
||||
options: Optional[Mapping[str, object]] = None,
|
||||
) -> AbstractJWKBase:
|
||||
try:
|
||||
return jwk_from_private_bytes(
|
||||
content,
|
||||
private_loader,
|
||||
password=private_password,
|
||||
backend=backend,
|
||||
options=options,
|
||||
)
|
||||
except UnsupportedKeyTypeError:
|
||||
return jwk_from_public_bytes(
|
||||
content,
|
||||
public_loader,
|
||||
backend=backend,
|
||||
options=options,
|
||||
)
|
||||
|
||||
|
||||
def jwk_from_pem(
|
||||
pem_content: bytes,
|
||||
private_password: Optional[str] = None,
|
||||
options: Optional[Mapping[str, object]] = None,
|
||||
) -> AbstractJWKBase:
|
||||
return jwk_from_bytes(
|
||||
pem_content,
|
||||
private_loader='load_pem_private_key',
|
||||
public_loader='load_pem_public_key',
|
||||
private_password=private_password,
|
||||
backend=None,
|
||||
options=options,
|
||||
)
|
||||
|
||||
|
||||
def jwk_from_der(
|
||||
der_content: bytes,
|
||||
private_password: Optional[str] = None,
|
||||
options: Optional[Mapping[str, object]] = None,
|
||||
) -> AbstractJWKBase:
|
||||
return jwk_from_bytes(
|
||||
der_content,
|
||||
private_loader='load_der_private_key',
|
||||
public_loader='load_der_public_key',
|
||||
private_password=private_password,
|
||||
backend=None,
|
||||
options=options,
|
||||
)
|
||||
54
billinglayer/python/jwt/jwkset.py
Normal file
54
billinglayer/python/jwt/jwkset.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections import UserList
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .jwk import AbstractJWKBase, jwk_from_dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
UserListBase = UserList[AbstractJWKBase]
|
||||
else:
|
||||
UserListBase = UserList
|
||||
|
||||
|
||||
class JWKSet(UserListBase):
|
||||
|
||||
def filter_keys(self, kid=None, kty=None):
|
||||
# When "kid" values are used within a JWK Set, different
|
||||
# keys within the JWK Set SHOULD use distinct "kid" values. (One
|
||||
# example in which different keys might use the same "kid" value is if
|
||||
# they have different "kty" (key type) values but are considered to be
|
||||
# equivalent alternatives by the application using them.)
|
||||
|
||||
if kid and kty:
|
||||
return [key for key in self.data
|
||||
if key.get_kty() == kty and key.get_kid() == kid]
|
||||
if kid:
|
||||
return [key for key in self.data if key.get_kid() == kid]
|
||||
if kty:
|
||||
return [key for key in self.data if key.get_kty() == kty]
|
||||
|
||||
return self.data.copy()
|
||||
|
||||
def to_dict(self, public_only=True):
|
||||
keys = [key.to_dict(public_only=public_only) for key in self.data]
|
||||
return {'keys': keys}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dct):
|
||||
keys = [jwk_from_dict(key_dct) for key_dct in dct.get('keys', [])]
|
||||
return cls(keys)
|
||||
104
billinglayer/python/jwt/jws.py
Normal file
104
billinglayer/python/jwt/jws.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
from typing import (
|
||||
AbstractSet,
|
||||
Dict,
|
||||
Optional,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from .exceptions import (
|
||||
JWSEncodeError,
|
||||
JWSDecodeError,
|
||||
)
|
||||
from .jwa import (
|
||||
supported_signing_algorithms,
|
||||
AbstractSigningAlgorithm,
|
||||
)
|
||||
from .jwk import AbstractJWKBase
|
||||
from .utils import (
|
||||
b64encode,
|
||||
b64decode,
|
||||
)
|
||||
|
||||
__all__ = ['JWS']
|
||||
|
||||
|
||||
class JWS:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._supported_algs = supported_signing_algorithms()
|
||||
|
||||
def _retrieve_alg(self, alg: str) -> AbstractSigningAlgorithm:
|
||||
try:
|
||||
return self._supported_algs[alg]
|
||||
except KeyError:
|
||||
raise JWSDecodeError('Unsupported signing algorithm.')
|
||||
|
||||
def encode(self, message: bytes, key: Optional[AbstractJWKBase] = None,
|
||||
alg='HS256',
|
||||
optional_headers: Optional[Dict[str, str]] = None) -> str:
|
||||
if alg not in self._supported_algs: # pragma: no cover
|
||||
raise JWSEncodeError('unsupported algorithm: {}'.format(alg))
|
||||
alg_impl = self._retrieve_alg(alg)
|
||||
|
||||
header = optional_headers.copy() if optional_headers else {}
|
||||
header['alg'] = alg
|
||||
|
||||
header_b64 = b64encode(
|
||||
json.dumps(header, separators=(',', ':')).encode('ascii'))
|
||||
message_b64 = b64encode(message)
|
||||
signing_message = header_b64 + '.' + message_b64
|
||||
|
||||
signature = alg_impl.sign(signing_message.encode('ascii'), key)
|
||||
signature_b64 = b64encode(signature)
|
||||
|
||||
return signing_message + '.' + signature_b64
|
||||
|
||||
def _decode_segments(
|
||||
self, message: str) -> Tuple[Dict[str, str], bytes, bytes, str]:
|
||||
try:
|
||||
signing_message, signature_b64 = message.rsplit('.', 1)
|
||||
header_b64, message_b64 = signing_message.split('.')
|
||||
except ValueError:
|
||||
raise JWSDecodeError('malformed JWS payload')
|
||||
|
||||
header = json.loads(b64decode(header_b64).decode('ascii'))
|
||||
message_bin = b64decode(message_b64)
|
||||
signature = b64decode(signature_b64)
|
||||
return header, message_bin, signature, signing_message
|
||||
|
||||
def decode(self, message: str, key: Optional[AbstractJWKBase] = None,
|
||||
do_verify=True,
|
||||
algorithms: Optional[AbstractSet[str]] = None) -> bytes:
|
||||
if algorithms is None:
|
||||
algorithms = set(supported_signing_algorithms().keys())
|
||||
|
||||
header, message_bin, signature, signing_message = \
|
||||
self._decode_segments(message)
|
||||
|
||||
alg_value = header['alg']
|
||||
if alg_value not in algorithms:
|
||||
raise JWSDecodeError('Unsupported signing algorithm.')
|
||||
|
||||
alg_impl = self._retrieve_alg(alg_value)
|
||||
if do_verify and not alg_impl.verify(
|
||||
signing_message.encode('ascii'), key, signature):
|
||||
raise JWSDecodeError('JWS passed could not be validated')
|
||||
|
||||
return message_bin
|
||||
119
billinglayer/python/jwt/jwt.py
Normal file
119
billinglayer/python/jwt/jwt.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import AbstractSet, Any, Dict, Optional
|
||||
|
||||
from jwt.utils import (
|
||||
get_time_from_int,
|
||||
)
|
||||
from .exceptions import (
|
||||
JWSEncodeError,
|
||||
JWSDecodeError,
|
||||
JWTEncodeError,
|
||||
JWTDecodeError,
|
||||
)
|
||||
from .jwk import AbstractJWKBase
|
||||
from .jws import JWS
|
||||
|
||||
|
||||
class JWT:
|
||||
|
||||
def __init__(self):
|
||||
self._jws = JWS()
|
||||
|
||||
def encode(self, payload: Dict[str, Any],
|
||||
key: Optional[AbstractJWKBase] = None, alg='HS256',
|
||||
optional_headers: Optional[Dict[str, str]] = None) -> str:
|
||||
if not isinstance(self, JWT): # pragma: no cover
|
||||
# https://github.com/GehirnInc/python-jwt/issues/15
|
||||
raise RuntimeError(
|
||||
'encode must be called on a jwt.JWT() instance. '
|
||||
'Do jwt.JWT().encode(...)')
|
||||
if not isinstance(payload, dict): # pragma: no cover
|
||||
raise TypeError('payload must be a dict')
|
||||
if not (key is None
|
||||
or isinstance(key, AbstractJWKBase)): # pragma: no cover
|
||||
raise TypeError(
|
||||
'key must be an instance of a class implements '
|
||||
'jwt.AbstractJWKBase')
|
||||
if not (optional_headers is None
|
||||
or isinstance(optional_headers, dict)): # pragma: no cover
|
||||
raise TypeError('optional_headers must be a dict')
|
||||
|
||||
try:
|
||||
message = json.dumps(payload).encode('utf-8')
|
||||
except ValueError as why:
|
||||
raise JWTEncodeError(
|
||||
'payload must be able to be encoded to JSON') from why
|
||||
|
||||
optional_headers = optional_headers and optional_headers.copy() or {}
|
||||
optional_headers['typ'] = 'JWT'
|
||||
try:
|
||||
return self._jws.encode(message, key, alg, optional_headers)
|
||||
except JWSEncodeError as why:
|
||||
raise JWTEncodeError('failed to encode to JWT') from why
|
||||
|
||||
def decode(self, message: str, key: Optional[AbstractJWKBase] = None,
|
||||
do_verify=True, algorithms: Optional[AbstractSet[str]] = None,
|
||||
do_time_check: bool = True) -> Dict[str, Any]:
|
||||
if not isinstance(self, JWT): # pragma: no cover
|
||||
# https://github.com/GehirnInc/python-jwt/issues/15
|
||||
raise RuntimeError(
|
||||
'decode must be called on a jwt.JWT() instance. '
|
||||
'Do jwt.JWT().decode(...)')
|
||||
if not isinstance(message, str): # pragma: no cover
|
||||
raise TypeError('message must be a str')
|
||||
if not (key is None
|
||||
or isinstance(key, AbstractJWKBase)): # pragma: no cover
|
||||
raise TypeError(
|
||||
'key must be an instance of a class implements '
|
||||
'jwt.AbstractJWKBase')
|
||||
|
||||
# utc now with timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
try:
|
||||
message_bin = self._jws.decode(message, key, do_verify, algorithms)
|
||||
except JWSDecodeError as why:
|
||||
raise JWTDecodeError('failed to decode JWT') from why
|
||||
try:
|
||||
payload = json.loads(message_bin.decode('utf-8'))
|
||||
except ValueError as why:
|
||||
raise JWTDecodeError(
|
||||
'a payload of the JWT is not valid JSON') from why
|
||||
|
||||
# The "exp" (expiration time) claim identifies the expiration time on
|
||||
# or after which the JWT MUST NOT be accepted for processing.
|
||||
if 'exp' in payload and do_time_check:
|
||||
try:
|
||||
exp = get_time_from_int(payload['exp'])
|
||||
except TypeError:
|
||||
raise JWTDecodeError("Invalid Expired value")
|
||||
if now >= exp:
|
||||
raise JWTDecodeError("JWT Expired")
|
||||
|
||||
# The "nbf" (not before) claim identifies the time before which the JWT
|
||||
# MUST NOT be accepted for processing.
|
||||
if 'nbf' in payload and do_time_check:
|
||||
try:
|
||||
nbf = get_time_from_int(payload['nbf'])
|
||||
except TypeError:
|
||||
raise JWTDecodeError('Invalid "Not valid yet" value')
|
||||
if now < nbf:
|
||||
raise JWTDecodeError("JWT Not valid yet")
|
||||
|
||||
return payload
|
||||
0
billinglayer/python/jwt/py.typed
Normal file
0
billinglayer/python/jwt/py.typed
Normal file
75
billinglayer/python/jwt/utils.py
Normal file
75
billinglayer/python/jwt/utils.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2017 Gehirn Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from base64 import (
|
||||
urlsafe_b64encode,
|
||||
urlsafe_b64decode,
|
||||
)
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def b64encode(s: bytes) -> str:
|
||||
s_bin = urlsafe_b64encode(s)
|
||||
s_bin = s_bin.replace(b'=', b'')
|
||||
return s_bin.decode('ascii')
|
||||
|
||||
|
||||
def b64decode(s: str) -> bytes:
|
||||
s_bin = s.encode('ascii')
|
||||
s_bin += b'=' * (4 - len(s_bin) % 4)
|
||||
return urlsafe_b64decode(s_bin)
|
||||
|
||||
|
||||
def uint_b64encode(value: int) -> str:
|
||||
length = 1
|
||||
rem = value >> 8
|
||||
while rem:
|
||||
length += 1
|
||||
rem >>= 8
|
||||
|
||||
uint_bin = value.to_bytes(length, 'big', signed=False)
|
||||
return b64encode(uint_bin)
|
||||
|
||||
|
||||
def uint_b64decode(uint_b64: str) -> int:
|
||||
uint_bin = b64decode(uint_b64)
|
||||
|
||||
value = 0
|
||||
for b in uint_bin:
|
||||
value <<= 8
|
||||
value += int(b)
|
||||
return value
|
||||
|
||||
|
||||
def get_time_from_int(value: int) -> datetime:
|
||||
"""
|
||||
:param value: seconds since the Epoch
|
||||
:return: datetime
|
||||
"""
|
||||
if not isinstance(value, int): # pragma: no cover
|
||||
raise TypeError('an int is required')
|
||||
return datetime.fromtimestamp(value, timezone.utc)
|
||||
|
||||
|
||||
def get_int_from_datetime(value: datetime) -> int:
|
||||
"""
|
||||
:param value: datetime with or without timezone, if don't contains timezone
|
||||
it will managed as it is UTC
|
||||
:return: Seconds since the Epoch
|
||||
"""
|
||||
if not isinstance(value, datetime): # pragma: no cover
|
||||
raise TypeError('a datetime is required')
|
||||
return int(value.timestamp())
|
||||
Reference in New Issue
Block a user