first commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""authlib.jose
|
||||
~~~~~~~~~~~~
|
||||
|
||||
JOSE implementation in Authlib. Tracking the status of JOSE specs at
|
||||
https://tools.ietf.org/wg/jose/
|
||||
"""
|
||||
|
||||
from authlib.deprecate import deprecate
|
||||
|
||||
from .errors import JoseError
|
||||
from .rfc7515 import JsonWebSignature
|
||||
from .rfc7515 import JWSAlgorithm
|
||||
from .rfc7515 import JWSHeader
|
||||
from .rfc7515 import JWSObject
|
||||
from .rfc7516 import JsonWebEncryption
|
||||
from .rfc7516 import JWEAlgorithm
|
||||
from .rfc7516 import JWEEncAlgorithm
|
||||
from .rfc7516 import JWEZipAlgorithm
|
||||
from .rfc7517 import JsonWebKey
|
||||
from .rfc7517 import Key
|
||||
from .rfc7517 import KeySet
|
||||
from .rfc7518 import ECDHESAlgorithm
|
||||
from .rfc7518 import ECKey
|
||||
from .rfc7518 import OctKey
|
||||
from .rfc7518 import RSAKey
|
||||
from .rfc7518 import register_jwe_rfc7518
|
||||
from .rfc7518 import register_jws_rfc7518
|
||||
from .rfc7519 import BaseClaims
|
||||
from .rfc7519 import JsonWebToken
|
||||
from .rfc7519 import JWTClaims
|
||||
from .rfc8037 import OKPKey
|
||||
from .rfc8037 import register_jws_rfc8037
|
||||
|
||||
deprecate(
|
||||
"authlib.jose module is deprecated, please use joserfc instead.", version="2.0.0"
|
||||
)
|
||||
|
||||
# register algorithms
|
||||
register_jws_rfc7518(JsonWebSignature)
|
||||
register_jws_rfc8037(JsonWebSignature)
|
||||
|
||||
register_jwe_rfc7518(JsonWebEncryption)
|
||||
|
||||
# attach algorithms
|
||||
ECDHESAlgorithm.ALLOWED_KEY_CLS = (ECKey, OKPKey)
|
||||
|
||||
# register supported keys
|
||||
JsonWebKey.JWK_KEY_CLS = {
|
||||
OctKey.kty: OctKey,
|
||||
RSAKey.kty: RSAKey,
|
||||
ECKey.kty: ECKey,
|
||||
OKPKey.kty: OKPKey,
|
||||
}
|
||||
|
||||
jwt = JsonWebToken(
|
||||
[
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES256K",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"EdDSA",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"JoseError",
|
||||
"JsonWebSignature",
|
||||
"JWSAlgorithm",
|
||||
"JWSHeader",
|
||||
"JWSObject",
|
||||
"JsonWebEncryption",
|
||||
"JWEAlgorithm",
|
||||
"JWEEncAlgorithm",
|
||||
"JWEZipAlgorithm",
|
||||
"JsonWebKey",
|
||||
"Key",
|
||||
"KeySet",
|
||||
"OctKey",
|
||||
"RSAKey",
|
||||
"ECKey",
|
||||
"OKPKey",
|
||||
"JsonWebToken",
|
||||
"BaseClaims",
|
||||
"JWTClaims",
|
||||
"jwt",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
from ._jwe_algorithms import JWE_DRAFT_ALG_ALGORITHMS
|
||||
from ._jwe_enc_cryptography import C20PEncAlgorithm
|
||||
|
||||
try:
|
||||
from ._jwe_enc_cryptodome import XC20PEncAlgorithm
|
||||
except ImportError:
|
||||
XC20PEncAlgorithm = None
|
||||
|
||||
|
||||
def register_jwe_draft(cls):
|
||||
for alg in JWE_DRAFT_ALG_ALGORITHMS:
|
||||
cls.register_algorithm(alg)
|
||||
|
||||
cls.register_algorithm(C20PEncAlgorithm(256)) # C20P
|
||||
if XC20PEncAlgorithm is not None:
|
||||
cls.register_algorithm(XC20PEncAlgorithm(256)) # XC20P
|
||||
|
||||
|
||||
__all__ = ["register_jwe_draft"]
|
||||
@@ -0,0 +1,216 @@
|
||||
import struct
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
|
||||
|
||||
from authlib.jose.errors import InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError
|
||||
from authlib.jose.rfc7516 import JWEAlgorithmWithTagAwareKeyAgreement
|
||||
from authlib.jose.rfc7518 import AESAlgorithm
|
||||
from authlib.jose.rfc7518 import CBCHS2EncAlgorithm
|
||||
from authlib.jose.rfc7518 import ECKey
|
||||
from authlib.jose.rfc7518 import u32be_len_input
|
||||
from authlib.jose.rfc8037 import OKPKey
|
||||
|
||||
|
||||
class ECDH1PUAlgorithm(JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
EXTRA_HEADERS = ["epk", "apu", "apv", "skid"]
|
||||
ALLOWED_KEY_CLS = (ECKey, OKPKey)
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04
|
||||
def __init__(self, key_size=None):
|
||||
if key_size is None:
|
||||
self.name = "ECDH-1PU"
|
||||
self.description = "ECDH-1PU in the Direct Key Agreement mode"
|
||||
else:
|
||||
self.name = f"ECDH-1PU+A{key_size}KW"
|
||||
self.description = (
|
||||
f"ECDH-1PU using Concat KDF and CEK wrapped with A{key_size}KW"
|
||||
)
|
||||
self.key_size = key_size
|
||||
self.aeskw = AESAlgorithm(key_size)
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
if isinstance(raw_data, self.ALLOWED_KEY_CLS):
|
||||
return raw_data
|
||||
return ECKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
epk = self._generate_ephemeral_key(key)
|
||||
h = self._prepare_headers(epk)
|
||||
preset = {"epk": epk, "header": h}
|
||||
if self.key_size is not None:
|
||||
cek = enc_alg.generate_cek()
|
||||
preset["cek"] = cek
|
||||
return preset
|
||||
|
||||
def compute_shared_key(self, shared_key_e, shared_key_s):
|
||||
return shared_key_e + shared_key_s
|
||||
|
||||
def compute_fixed_info(self, headers, bit_size, tag):
|
||||
if tag is None:
|
||||
cctag = b""
|
||||
else:
|
||||
cctag = u32be_len_input(tag)
|
||||
|
||||
# AlgorithmID
|
||||
if self.key_size is None:
|
||||
alg_id = u32be_len_input(headers["enc"])
|
||||
else:
|
||||
alg_id = u32be_len_input(headers["alg"])
|
||||
|
||||
# PartyUInfo
|
||||
apu_info = u32be_len_input(headers.get("apu"), True)
|
||||
|
||||
# PartyVInfo
|
||||
apv_info = u32be_len_input(headers.get("apv"), True)
|
||||
|
||||
# SuppPubInfo
|
||||
pub_info = struct.pack(">I", bit_size) + cctag
|
||||
|
||||
return alg_id + apu_info + apv_info + pub_info
|
||||
|
||||
def compute_derived_key(self, shared_key, fixed_info, bit_size):
|
||||
ckdf = ConcatKDFHash(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=bit_size // 8,
|
||||
otherinfo=fixed_info,
|
||||
backend=default_backend(),
|
||||
)
|
||||
return ckdf.derive(shared_key)
|
||||
|
||||
def deliver_at_sender(
|
||||
self,
|
||||
sender_static_key,
|
||||
sender_ephemeral_key,
|
||||
recipient_pubkey,
|
||||
headers,
|
||||
bit_size,
|
||||
tag,
|
||||
):
|
||||
shared_key_s = sender_static_key.exchange_shared_key(recipient_pubkey)
|
||||
shared_key_e = sender_ephemeral_key.exchange_shared_key(recipient_pubkey)
|
||||
shared_key = self.compute_shared_key(shared_key_e, shared_key_s)
|
||||
|
||||
fixed_info = self.compute_fixed_info(headers, bit_size, tag)
|
||||
|
||||
return self.compute_derived_key(shared_key, fixed_info, bit_size)
|
||||
|
||||
def deliver_at_recipient(
|
||||
self,
|
||||
recipient_key,
|
||||
sender_static_pubkey,
|
||||
sender_ephemeral_pubkey,
|
||||
headers,
|
||||
bit_size,
|
||||
tag,
|
||||
):
|
||||
shared_key_s = recipient_key.exchange_shared_key(sender_static_pubkey)
|
||||
shared_key_e = recipient_key.exchange_shared_key(sender_ephemeral_pubkey)
|
||||
shared_key = self.compute_shared_key(shared_key_e, shared_key_s)
|
||||
|
||||
fixed_info = self.compute_fixed_info(headers, bit_size, tag)
|
||||
|
||||
return self.compute_derived_key(shared_key, fixed_info, bit_size)
|
||||
|
||||
def _generate_ephemeral_key(self, key):
|
||||
return key.generate_key(key["crv"], is_private=True)
|
||||
|
||||
def _prepare_headers(self, epk):
|
||||
# REQUIRED_JSON_FIELDS contains only public fields
|
||||
pub_epk = {k: epk[k] for k in epk.REQUIRED_JSON_FIELDS}
|
||||
pub_epk["kty"] = epk.kty
|
||||
return {"epk": pub_epk}
|
||||
|
||||
def generate_keys_and_prepare_headers(self, enc_alg, key, sender_key, preset=None):
|
||||
if not isinstance(enc_alg, CBCHS2EncAlgorithm):
|
||||
raise InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError()
|
||||
|
||||
if preset and "epk" in preset:
|
||||
epk = preset["epk"]
|
||||
h = {}
|
||||
else:
|
||||
epk = self._generate_ephemeral_key(key)
|
||||
h = self._prepare_headers(epk)
|
||||
|
||||
if preset and "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = enc_alg.generate_cek()
|
||||
|
||||
return {"epk": epk, "cek": cek, "header": h}
|
||||
|
||||
def _agree_upon_key_at_sender(
|
||||
self, enc_alg, headers, key, sender_key, epk, tag=None
|
||||
):
|
||||
if self.key_size is None:
|
||||
bit_size = enc_alg.CEK_SIZE
|
||||
else:
|
||||
bit_size = self.key_size
|
||||
|
||||
public_key = key.get_op_key("wrapKey")
|
||||
|
||||
return self.deliver_at_sender(
|
||||
sender_key, epk, public_key, headers, bit_size, tag
|
||||
)
|
||||
|
||||
def _wrap_cek(self, cek, dk):
|
||||
kek = self.aeskw.prepare_key(dk)
|
||||
return self.aeskw.wrap_cek(cek, kek)
|
||||
|
||||
def agree_upon_key_and_wrap_cek(
|
||||
self, enc_alg, headers, key, sender_key, epk, cek, tag
|
||||
):
|
||||
dk = self._agree_upon_key_at_sender(enc_alg, headers, key, sender_key, epk, tag)
|
||||
return self._wrap_cek(cek, dk)
|
||||
|
||||
def wrap(self, enc_alg, headers, key, sender_key, preset=None):
|
||||
# In this class this method is used in direct key agreement mode only
|
||||
if self.key_size is not None:
|
||||
raise RuntimeError("Invalid algorithm state detected")
|
||||
|
||||
if preset and "epk" in preset:
|
||||
epk = preset["epk"]
|
||||
h = {}
|
||||
else:
|
||||
epk = self._generate_ephemeral_key(key)
|
||||
h = self._prepare_headers(epk)
|
||||
|
||||
dk = self._agree_upon_key_at_sender(enc_alg, headers, key, sender_key, epk)
|
||||
|
||||
return {"ek": b"", "cek": dk, "header": h}
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key, sender_key, tag=None):
|
||||
if "epk" not in headers:
|
||||
raise ValueError('Missing "epk" in headers')
|
||||
|
||||
if self.key_size is None:
|
||||
bit_size = enc_alg.CEK_SIZE
|
||||
else:
|
||||
bit_size = self.key_size
|
||||
|
||||
sender_pubkey = sender_key.get_op_key("wrapKey")
|
||||
epk = key.import_key(headers["epk"])
|
||||
epk_pubkey = epk.get_op_key("wrapKey")
|
||||
dk = self.deliver_at_recipient(
|
||||
key, sender_pubkey, epk_pubkey, headers, bit_size, tag
|
||||
)
|
||||
|
||||
if self.key_size is None:
|
||||
return dk
|
||||
|
||||
kek = self.aeskw.prepare_key(dk)
|
||||
return self.aeskw.unwrap(enc_alg, ek, headers, kek)
|
||||
|
||||
|
||||
JWE_DRAFT_ALG_ALGORITHMS = [
|
||||
ECDH1PUAlgorithm(None), # ECDH-1PU
|
||||
ECDH1PUAlgorithm(128), # ECDH-1PU+A128KW
|
||||
ECDH1PUAlgorithm(192), # ECDH-1PU+A192KW
|
||||
ECDH1PUAlgorithm(256), # ECDH-1PU+A256KW
|
||||
]
|
||||
|
||||
|
||||
def register_jwe_alg_draft(cls):
|
||||
for alg in JWE_DRAFT_ALG_ALGORITHMS:
|
||||
cls.register_algorithm(alg)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""authlib.jose.draft.
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Content Encryption per `Section 4`_.
|
||||
|
||||
.. _`Section 4`: https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4
|
||||
"""
|
||||
|
||||
from Cryptodome.Cipher import ChaCha20_Poly1305 as Cryptodome_ChaCha20_Poly1305
|
||||
|
||||
from authlib.jose.rfc7516 import JWEEncAlgorithm
|
||||
|
||||
|
||||
class XC20PEncAlgorithm(JWEEncAlgorithm):
|
||||
# Use of an IV of size 192 bits is REQUIRED with this algorithm.
|
||||
# https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4.1
|
||||
IV_SIZE = 192
|
||||
|
||||
def __init__(self, key_size):
|
||||
self.name = "XC20P"
|
||||
self.description = "XChaCha20-Poly1305"
|
||||
self.key_size = key_size
|
||||
self.CEK_SIZE = key_size
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Content Encryption with AEAD_XCHACHA20_POLY1305.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, tag)
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
chacha = Cryptodome_ChaCha20_Poly1305.new(key=key, nonce=iv)
|
||||
chacha.update(aad)
|
||||
ciphertext, tag = chacha.encrypt_and_digest(msg)
|
||||
return ciphertext, tag
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Content Decryption with AEAD_XCHACHA20_POLY1305.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
chacha = Cryptodome_ChaCha20_Poly1305.new(key=key, nonce=iv)
|
||||
chacha.update(aad)
|
||||
return chacha.decrypt_and_verify(ciphertext, tag)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""authlib.jose.draft.
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Content Encryption per `Section 4`_.
|
||||
|
||||
.. _`Section 4`: https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4
|
||||
"""
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
||||
|
||||
from authlib.jose.rfc7516 import JWEEncAlgorithm
|
||||
|
||||
|
||||
class C20PEncAlgorithm(JWEEncAlgorithm):
|
||||
# Use of an IV of size 96 bits is REQUIRED with this algorithm.
|
||||
# https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4.1
|
||||
IV_SIZE = 96
|
||||
|
||||
def __init__(self, key_size):
|
||||
self.name = "C20P"
|
||||
self.description = "ChaCha20-Poly1305"
|
||||
self.key_size = key_size
|
||||
self.CEK_SIZE = key_size
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Content Encryption with AEAD_CHACHA20_POLY1305.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, tag)
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
chacha = ChaCha20Poly1305(key)
|
||||
ciphertext = chacha.encrypt(iv, msg, aad)
|
||||
return ciphertext[:-16], ciphertext[-16:]
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Content Decryption with AEAD_CHACHA20_POLY1305.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
chacha = ChaCha20Poly1305(key)
|
||||
return chacha.decrypt(iv, ciphertext + tag, aad)
|
||||
@@ -0,0 +1,120 @@
|
||||
from authlib.common.errors import AuthlibBaseError
|
||||
|
||||
|
||||
class JoseError(AuthlibBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class DecodeError(JoseError):
|
||||
error = "decode_error"
|
||||
|
||||
|
||||
class MissingAlgorithmError(JoseError):
|
||||
error = "missing_algorithm"
|
||||
|
||||
|
||||
class UnsupportedAlgorithmError(JoseError):
|
||||
error = "unsupported_algorithm"
|
||||
|
||||
|
||||
class BadSignatureError(JoseError):
|
||||
error = "bad_signature"
|
||||
|
||||
def __init__(self, result):
|
||||
super().__init__()
|
||||
self.result = result
|
||||
|
||||
|
||||
class InvalidHeaderParameterNameError(JoseError):
|
||||
error = "invalid_header_parameter_name"
|
||||
|
||||
def __init__(self, name):
|
||||
description = f"Invalid Header Parameter Name: {name}"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class InvalidCritHeaderParameterNameError(JoseError):
|
||||
error = "invalid_crit_header_parameter_name"
|
||||
|
||||
def __init__(self, name):
|
||||
description = f"Invalid Header Parameter Name: {name}"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError(JoseError):
|
||||
error = "invalid_encryption_algorithm_for_ECDH_1PU_with_key_wrapping"
|
||||
|
||||
def __init__(self):
|
||||
description = (
|
||||
"In key agreement with key wrapping mode ECDH-1PU algorithm "
|
||||
"only supports AES_CBC_HMAC_SHA2 family encryption algorithms"
|
||||
)
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class InvalidAlgorithmForMultipleRecipientsMode(JoseError):
|
||||
error = "invalid_algorithm_for_multiple_recipients_mode"
|
||||
|
||||
def __init__(self, alg):
|
||||
description = f"{alg} algorithm cannot be used in multiple recipients mode"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class KeyMismatchError(JoseError):
|
||||
error = "key_mismatch_error"
|
||||
description = "Key does not match to any recipient"
|
||||
|
||||
|
||||
class MissingEncryptionAlgorithmError(JoseError):
|
||||
error = "missing_encryption_algorithm"
|
||||
description = "Missing 'enc' in header"
|
||||
|
||||
|
||||
class UnsupportedEncryptionAlgorithmError(JoseError):
|
||||
error = "unsupported_encryption_algorithm"
|
||||
description = "Unsupported 'enc' value in header"
|
||||
|
||||
|
||||
class UnsupportedCompressionAlgorithmError(JoseError):
|
||||
error = "unsupported_compression_algorithm"
|
||||
description = "Unsupported 'zip' value in header"
|
||||
|
||||
|
||||
class InvalidUseError(JoseError):
|
||||
error = "invalid_use"
|
||||
description = "Key 'use' is not valid for your usage"
|
||||
|
||||
|
||||
class InvalidClaimError(JoseError):
|
||||
error = "invalid_claim"
|
||||
|
||||
def __init__(self, claim):
|
||||
self.claim_name = claim
|
||||
description = f"Invalid claim '{claim}'"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class MissingClaimError(JoseError):
|
||||
error = "missing_claim"
|
||||
|
||||
def __init__(self, claim):
|
||||
description = f"Missing '{claim}' claim"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class InsecureClaimError(JoseError):
|
||||
error = "insecure_claim"
|
||||
|
||||
def __init__(self, claim):
|
||||
description = f"Insecure claim '{claim}'"
|
||||
super().__init__(description=description)
|
||||
|
||||
|
||||
class ExpiredTokenError(JoseError):
|
||||
error = "expired_token"
|
||||
description = "The token is expired"
|
||||
|
||||
|
||||
class InvalidTokenError(JoseError):
|
||||
error = "invalid_token"
|
||||
description = "The token is not valid yet"
|
||||
@@ -0,0 +1,20 @@
|
||||
from authlib.deprecate import deprecate
|
||||
|
||||
from .rfc7517 import JsonWebKey
|
||||
|
||||
|
||||
def loads(obj, kid=None):
|
||||
deprecate("Please use ``JsonWebKey`` directly.")
|
||||
key_set = JsonWebKey.import_key_set(obj)
|
||||
if key_set:
|
||||
return key_set.find_by_kid(kid)
|
||||
return JsonWebKey.import_key(obj)
|
||||
|
||||
|
||||
def dumps(key, kty=None, **params):
|
||||
deprecate("Please use ``JsonWebKey`` directly.")
|
||||
if kty:
|
||||
params["kty"] = kty
|
||||
|
||||
key = JsonWebKey.import_key(key, params)
|
||||
return dict(key)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""authlib.jose.rfc7515.
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
JSON Web Signature (JWS).
|
||||
|
||||
https://tools.ietf.org/html/rfc7515
|
||||
"""
|
||||
|
||||
from .jws import JsonWebSignature
|
||||
from .models import JWSAlgorithm
|
||||
from .models import JWSHeader
|
||||
from .models import JWSObject
|
||||
|
||||
__all__ = ["JsonWebSignature", "JWSAlgorithm", "JWSHeader", "JWSObject"]
|
||||
@@ -0,0 +1,352 @@
|
||||
from authlib.common.encoding import json_b64encode
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
from authlib.jose.errors import BadSignatureError
|
||||
from authlib.jose.errors import DecodeError
|
||||
from authlib.jose.errors import InvalidCritHeaderParameterNameError
|
||||
from authlib.jose.errors import InvalidHeaderParameterNameError
|
||||
from authlib.jose.errors import MissingAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedAlgorithmError
|
||||
from authlib.jose.util import ensure_dict
|
||||
from authlib.jose.util import extract_header
|
||||
from authlib.jose.util import extract_segment
|
||||
|
||||
from .models import JWSHeader
|
||||
from .models import JWSObject
|
||||
|
||||
|
||||
class JsonWebSignature:
|
||||
#: Registered Header Parameter Names defined by Section 4.1
|
||||
REGISTERED_HEADER_PARAMETER_NAMES = frozenset(
|
||||
[
|
||||
"alg",
|
||||
"jku",
|
||||
"jwk",
|
||||
"kid",
|
||||
"x5u",
|
||||
"x5c",
|
||||
"x5t",
|
||||
"x5t#S256",
|
||||
"typ",
|
||||
"cty",
|
||||
"crit",
|
||||
]
|
||||
)
|
||||
|
||||
MAX_CONTENT_LENGTH: int = 256000
|
||||
|
||||
#: Defined available JWS algorithms in the registry
|
||||
ALGORITHMS_REGISTRY = {}
|
||||
|
||||
def __init__(self, algorithms=None, private_headers=None):
|
||||
self._private_headers = private_headers
|
||||
self._algorithms = algorithms
|
||||
|
||||
@classmethod
|
||||
def register_algorithm(cls, algorithm):
|
||||
if not algorithm or algorithm.algorithm_type != "JWS":
|
||||
raise ValueError(f"Invalid algorithm for JWS, {algorithm!r}")
|
||||
cls.ALGORITHMS_REGISTRY[algorithm.name] = algorithm
|
||||
|
||||
def serialize_compact(self, protected, payload, key):
|
||||
"""Generate a JWS Compact Serialization. The JWS Compact Serialization
|
||||
represents digitally signed or MACed content as a compact, URL-safe
|
||||
string, per `Section 7.1`_.
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
BASE64URL(UTF8(JWS Protected Header)) || '.' ||
|
||||
BASE64URL(JWS Payload) || '.' ||
|
||||
BASE64URL(JWS Signature)
|
||||
|
||||
:param protected: A dict of protected header
|
||||
:param payload: A bytes/string of payload
|
||||
:param key: Private key used to generate signature
|
||||
:return: byte
|
||||
"""
|
||||
jws_header = JWSHeader(protected, None)
|
||||
self._validate_private_headers(protected)
|
||||
self._validate_crit_headers(protected)
|
||||
algorithm, key = self._prepare_algorithm_key(protected, payload, key)
|
||||
|
||||
protected_segment = json_b64encode(jws_header.protected)
|
||||
payload_segment = urlsafe_b64encode(to_bytes(payload))
|
||||
|
||||
# calculate signature
|
||||
signing_input = b".".join([protected_segment, payload_segment])
|
||||
signature = urlsafe_b64encode(algorithm.sign(signing_input, key))
|
||||
return b".".join([protected_segment, payload_segment, signature])
|
||||
|
||||
def deserialize_compact(self, s, key, decode=None):
|
||||
"""Exact JWS Compact Serialization, and validate with the given key.
|
||||
If key is not provided, the returned dict will contain the signature,
|
||||
and signing input values. Via `Section 7.1`_.
|
||||
|
||||
:param s: text of JWS Compact Serialization
|
||||
:param key: key used to verify the signature
|
||||
:param decode: a function to decode payload data
|
||||
:return: JWSObject
|
||||
:raise: BadSignatureError
|
||||
|
||||
.. _`Section 7.1`: https://tools.ietf.org/html/rfc7515#section-7.1
|
||||
"""
|
||||
if len(s) > self.MAX_CONTENT_LENGTH:
|
||||
raise ValueError("Serialization is too long.")
|
||||
|
||||
try:
|
||||
s = to_bytes(s)
|
||||
signing_input, signature_segment = s.rsplit(b".", 1)
|
||||
protected_segment, payload_segment = signing_input.split(b".", 1)
|
||||
except ValueError as exc:
|
||||
raise DecodeError("Not enough segments") from exc
|
||||
|
||||
protected = _extract_header(protected_segment)
|
||||
self._validate_crit_headers(protected)
|
||||
jws_header = JWSHeader(protected, None)
|
||||
|
||||
payload = _extract_payload(payload_segment)
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
|
||||
signature = _extract_signature(signature_segment)
|
||||
rv = JWSObject(jws_header, payload, "compact")
|
||||
algorithm, key = self._prepare_algorithm_key(jws_header, payload, key)
|
||||
if algorithm.verify(signing_input, signature, key):
|
||||
return rv
|
||||
raise BadSignatureError(rv)
|
||||
|
||||
def serialize_json(self, header_obj, payload, key):
|
||||
"""Generate a JWS JSON Serialization. The JWS JSON Serialization
|
||||
represents digitally signed or MACed content as a JSON object,
|
||||
per `Section 7.2`_.
|
||||
|
||||
:param header_obj: A dict/list of header
|
||||
:param payload: A string/dict of payload
|
||||
:param key: Private key used to generate signature
|
||||
:return: JWSObject
|
||||
|
||||
Example ``header_obj`` of JWS JSON Serialization::
|
||||
|
||||
{
|
||||
"protected: {"alg": "HS256"},
|
||||
"header": {"kid": "jose"}
|
||||
}
|
||||
|
||||
Pass a dict to generate flattened JSON Serialization, pass a list of
|
||||
header dict to generate standard JSON Serialization.
|
||||
"""
|
||||
payload_segment = json_b64encode(payload)
|
||||
|
||||
def _sign(jws_header):
|
||||
self._validate_private_headers(jws_header)
|
||||
# RFC 7515 §4.1.11: 'crit' MUST be integrity-protected.
|
||||
# Reject if present in unprotected header, and validate only
|
||||
# against the protected header parameters.
|
||||
self._reject_unprotected_crit(jws_header.header)
|
||||
self._validate_crit_headers(jws_header.protected)
|
||||
_alg, _key = self._prepare_algorithm_key(jws_header, payload, key)
|
||||
|
||||
protected_segment = json_b64encode(jws_header.protected)
|
||||
signing_input = b".".join([protected_segment, payload_segment])
|
||||
signature = urlsafe_b64encode(_alg.sign(signing_input, _key))
|
||||
|
||||
rv = {
|
||||
"protected": to_unicode(protected_segment),
|
||||
"signature": to_unicode(signature),
|
||||
}
|
||||
if jws_header.header is not None:
|
||||
rv["header"] = jws_header.header
|
||||
return rv
|
||||
|
||||
if isinstance(header_obj, dict):
|
||||
data = _sign(JWSHeader.from_dict(header_obj))
|
||||
data["payload"] = to_unicode(payload_segment)
|
||||
return data
|
||||
|
||||
signatures = [_sign(JWSHeader.from_dict(h)) for h in header_obj]
|
||||
return {"payload": to_unicode(payload_segment), "signatures": signatures}
|
||||
|
||||
def deserialize_json(self, obj, key, decode=None):
|
||||
"""Exact JWS JSON Serialization, and validate with the given key.
|
||||
If key is not provided, it will return a dict without signature
|
||||
verification. Header will still be validated. Via `Section 7.2`_.
|
||||
|
||||
:param obj: text of JWS JSON Serialization
|
||||
:param key: key used to verify the signature
|
||||
:param decode: a function to decode payload data
|
||||
:return: JWSObject
|
||||
:raise: BadSignatureError
|
||||
|
||||
.. _`Section 7.2`: https://tools.ietf.org/html/rfc7515#section-7.2
|
||||
"""
|
||||
obj = ensure_dict(obj, "JWS")
|
||||
|
||||
payload_segment = obj.get("payload")
|
||||
if payload_segment is None:
|
||||
raise DecodeError('Missing "payload" value')
|
||||
|
||||
payload_segment = to_bytes(payload_segment)
|
||||
payload = _extract_payload(payload_segment)
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
|
||||
if "signatures" not in obj:
|
||||
# flattened JSON JWS
|
||||
jws_header, valid = self._validate_json_jws(
|
||||
payload_segment, payload, obj, key
|
||||
)
|
||||
|
||||
rv = JWSObject(jws_header, payload, "flat")
|
||||
if valid:
|
||||
return rv
|
||||
raise BadSignatureError(rv)
|
||||
|
||||
headers = []
|
||||
is_valid = True
|
||||
for header_obj in obj["signatures"]:
|
||||
jws_header, valid = self._validate_json_jws(
|
||||
payload_segment, payload, header_obj, key
|
||||
)
|
||||
headers.append(jws_header)
|
||||
if not valid:
|
||||
is_valid = False
|
||||
|
||||
rv = JWSObject(headers, payload, "json")
|
||||
if is_valid:
|
||||
return rv
|
||||
raise BadSignatureError(rv)
|
||||
|
||||
def serialize(self, header, payload, key):
|
||||
"""Generate a JWS Serialization. It will automatically generate a
|
||||
Compact or JSON Serialization depending on the given header. If a
|
||||
header is in a JSON header format, it will call
|
||||
:meth:`serialize_json`, otherwise it will call
|
||||
:meth:`serialize_compact`.
|
||||
|
||||
:param header: A dict/list of header
|
||||
:param payload: A string/dict of payload
|
||||
:param key: Private key used to generate signature
|
||||
:return: byte/dict
|
||||
"""
|
||||
if isinstance(header, (list, tuple)):
|
||||
return self.serialize_json(header, payload, key)
|
||||
if "protected" in header:
|
||||
return self.serialize_json(header, payload, key)
|
||||
return self.serialize_compact(header, payload, key)
|
||||
|
||||
def deserialize(self, s, key, decode=None):
|
||||
"""Deserialize JWS Serialization, both compact and JSON format.
|
||||
It will automatically deserialize depending on the given JWS.
|
||||
|
||||
:param s: text of JWS Compact/JSON Serialization
|
||||
:param key: key used to verify the signature
|
||||
:param decode: a function to decode payload data
|
||||
:return: dict
|
||||
:raise: BadSignatureError
|
||||
|
||||
If key is not provided, it will still deserialize the serialization
|
||||
without verification.
|
||||
"""
|
||||
if isinstance(s, dict):
|
||||
return self.deserialize_json(s, key, decode)
|
||||
|
||||
s = to_bytes(s)
|
||||
if s.startswith(b"{") and s.endswith(b"}"):
|
||||
return self.deserialize_json(s, key, decode)
|
||||
return self.deserialize_compact(s, key, decode)
|
||||
|
||||
def _prepare_algorithm_key(self, header, payload, key):
|
||||
if "alg" not in header:
|
||||
raise MissingAlgorithmError()
|
||||
|
||||
alg = header["alg"]
|
||||
if alg not in self.ALGORITHMS_REGISTRY:
|
||||
raise UnsupportedAlgorithmError()
|
||||
|
||||
algorithm = self.ALGORITHMS_REGISTRY[alg]
|
||||
if self._algorithms is None:
|
||||
if algorithm.deprecated:
|
||||
raise UnsupportedAlgorithmError()
|
||||
elif alg not in self._algorithms:
|
||||
raise UnsupportedAlgorithmError()
|
||||
|
||||
if callable(key):
|
||||
key = key(header, payload)
|
||||
key = algorithm.prepare_key(key)
|
||||
return algorithm, key
|
||||
|
||||
def _validate_private_headers(self, header):
|
||||
# only validate private headers when developers set
|
||||
# private headers explicitly
|
||||
if self._private_headers is not None:
|
||||
names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy()
|
||||
names = names.union(self._private_headers)
|
||||
|
||||
for k in header:
|
||||
if k not in names:
|
||||
raise InvalidHeaderParameterNameError(k)
|
||||
|
||||
def _reject_unprotected_crit(self, unprotected_header):
|
||||
"""Reject 'crit' when found in the unprotected header (RFC 7515 §4.1.11)."""
|
||||
if unprotected_header and "crit" in unprotected_header:
|
||||
raise InvalidHeaderParameterNameError("crit")
|
||||
|
||||
def _validate_crit_headers(self, header):
|
||||
if "crit" in header:
|
||||
crit_headers = header["crit"]
|
||||
# Type enforcement for robustness and predictable errors
|
||||
if not isinstance(crit_headers, list) or not all(
|
||||
isinstance(x, str) for x in crit_headers
|
||||
):
|
||||
raise InvalidHeaderParameterNameError("crit")
|
||||
names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy()
|
||||
if self._private_headers:
|
||||
names = names.union(self._private_headers)
|
||||
for k in crit_headers:
|
||||
if k not in names:
|
||||
raise InvalidCritHeaderParameterNameError(k)
|
||||
elif k not in header:
|
||||
raise InvalidCritHeaderParameterNameError(k)
|
||||
|
||||
def _validate_json_jws(self, payload_segment, payload, header_obj, key):
|
||||
protected_segment = header_obj.get("protected")
|
||||
if not protected_segment:
|
||||
raise DecodeError('Missing "protected" value')
|
||||
|
||||
signature_segment = header_obj.get("signature")
|
||||
if not signature_segment:
|
||||
raise DecodeError('Missing "signature" value')
|
||||
|
||||
protected_segment = to_bytes(protected_segment)
|
||||
protected = _extract_header(protected_segment)
|
||||
header = header_obj.get("header")
|
||||
if header and not isinstance(header, dict):
|
||||
raise DecodeError('Invalid "header" value')
|
||||
# RFC 7515 §4.1.11: 'crit' MUST be integrity-protected. If present in
|
||||
# the unprotected header object, reject the JWS.
|
||||
self._reject_unprotected_crit(header)
|
||||
|
||||
# Enforce must-understand semantics for names listed in protected
|
||||
# 'crit'. This will also ensure each listed name is present in the
|
||||
# protected header.
|
||||
self._validate_crit_headers(protected)
|
||||
jws_header = JWSHeader(protected, header)
|
||||
algorithm, key = self._prepare_algorithm_key(jws_header, payload, key)
|
||||
signing_input = b".".join([protected_segment, payload_segment])
|
||||
signature = _extract_signature(to_bytes(signature_segment))
|
||||
if algorithm.verify(signing_input, signature, key):
|
||||
return jws_header, True
|
||||
return jws_header, False
|
||||
|
||||
|
||||
def _extract_header(header_segment):
|
||||
return extract_header(header_segment, DecodeError)
|
||||
|
||||
|
||||
def _extract_signature(signature_segment):
|
||||
return extract_segment(signature_segment, DecodeError, "signature")
|
||||
|
||||
|
||||
def _extract_payload(payload_segment):
|
||||
return extract_segment(payload_segment, DecodeError, "payload")
|
||||
@@ -0,0 +1,85 @@
|
||||
class JWSAlgorithm:
|
||||
"""Interface for JWS algorithm. JWA specification (RFC7518) SHOULD
|
||||
implement the algorithms for JWS with this base implementation.
|
||||
"""
|
||||
|
||||
name = None
|
||||
description = None
|
||||
deprecated = False
|
||||
algorithm_type = "JWS"
|
||||
algorithm_location = "alg"
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
"""Prepare key for signing and verifying signature."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def sign(self, msg, key):
|
||||
"""Sign the text msg with a private/sign key.
|
||||
|
||||
:param msg: message bytes to be signed
|
||||
:param key: private key to sign the message
|
||||
:return: bytes
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
"""Verify the signature of text msg with a public/verify key.
|
||||
|
||||
:param msg: message bytes to be signed
|
||||
:param sig: result signature to be compared
|
||||
:param key: public key to verify the signature
|
||||
:return: boolean
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWSHeader(dict):
|
||||
"""Header object for JWS. It combine the protected header and unprotected
|
||||
header together. JWSHeader itself is a dict of the combined dict. e.g.
|
||||
|
||||
>>> protected = {"alg": "HS256"}
|
||||
>>> header = {"kid": "a"}
|
||||
>>> jws_header = JWSHeader(protected, header)
|
||||
>>> print(jws_header)
|
||||
{'alg': 'HS256', 'kid': 'a'}
|
||||
>>> jws_header.protected == protected
|
||||
>>> jws_header.header == header
|
||||
|
||||
:param protected: dict of protected header
|
||||
:param header: dict of unprotected header
|
||||
"""
|
||||
|
||||
def __init__(self, protected, header):
|
||||
obj = {}
|
||||
if header:
|
||||
obj.update(header)
|
||||
if protected:
|
||||
obj.update(protected)
|
||||
super().__init__(obj)
|
||||
self.protected = protected
|
||||
self.header = header
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj):
|
||||
if isinstance(obj, cls):
|
||||
return obj
|
||||
return cls(obj.get("protected"), obj.get("header"))
|
||||
|
||||
|
||||
class JWSObject(dict):
|
||||
"""A dict instance to represent a JWS object."""
|
||||
|
||||
def __init__(self, header, payload, type="compact"):
|
||||
super().__init__(
|
||||
header=header,
|
||||
payload=payload,
|
||||
)
|
||||
self.header = header
|
||||
self.payload = payload
|
||||
self.type = type
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Alias of ``header`` for JSON typed JWS."""
|
||||
if self.type == "json":
|
||||
return self["header"]
|
||||
@@ -0,0 +1,22 @@
|
||||
"""authlib.jose.rfc7516.
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
JSON Web Encryption (JWE).
|
||||
|
||||
https://tools.ietf.org/html/rfc7516
|
||||
"""
|
||||
|
||||
from .jwe import JsonWebEncryption
|
||||
from .models import JWEAlgorithm
|
||||
from .models import JWEAlgorithmWithTagAwareKeyAgreement
|
||||
from .models import JWEEncAlgorithm
|
||||
from .models import JWEZipAlgorithm
|
||||
|
||||
__all__ = [
|
||||
"JsonWebEncryption",
|
||||
"JWEAlgorithm",
|
||||
"JWEAlgorithmWithTagAwareKeyAgreement",
|
||||
"JWEEncAlgorithm",
|
||||
"JWEZipAlgorithm",
|
||||
]
|
||||
@@ -0,0 +1,765 @@
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
|
||||
from authlib.common.encoding import json_b64encode
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
from authlib.jose.errors import DecodeError
|
||||
from authlib.jose.errors import InvalidAlgorithmForMultipleRecipientsMode
|
||||
from authlib.jose.errors import InvalidHeaderParameterNameError
|
||||
from authlib.jose.errors import KeyMismatchError
|
||||
from authlib.jose.errors import MissingAlgorithmError
|
||||
from authlib.jose.errors import MissingEncryptionAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedCompressionAlgorithmError
|
||||
from authlib.jose.errors import UnsupportedEncryptionAlgorithmError
|
||||
from authlib.jose.rfc7516.models import JWEAlgorithmWithTagAwareKeyAgreement
|
||||
from authlib.jose.rfc7516.models import JWEHeader
|
||||
from authlib.jose.rfc7516.models import JWESharedHeader
|
||||
from authlib.jose.util import ensure_dict
|
||||
from authlib.jose.util import extract_header
|
||||
from authlib.jose.util import extract_segment
|
||||
|
||||
|
||||
class JsonWebEncryption:
|
||||
#: Registered Header Parameter Names defined by Section 4.1
|
||||
REGISTERED_HEADER_PARAMETER_NAMES = frozenset(
|
||||
[
|
||||
"alg",
|
||||
"enc",
|
||||
"zip",
|
||||
"jku",
|
||||
"jwk",
|
||||
"kid",
|
||||
"x5u",
|
||||
"x5c",
|
||||
"x5t",
|
||||
"x5t#S256",
|
||||
"typ",
|
||||
"cty",
|
||||
"crit",
|
||||
]
|
||||
)
|
||||
|
||||
ALG_REGISTRY = {}
|
||||
ENC_REGISTRY = {}
|
||||
ZIP_REGISTRY = {}
|
||||
|
||||
def __init__(self, algorithms=None, private_headers=None):
|
||||
self._algorithms = algorithms
|
||||
self._private_headers = private_headers
|
||||
|
||||
@classmethod
|
||||
def register_algorithm(cls, algorithm):
|
||||
"""Register an algorithm for ``alg`` or ``enc`` or ``zip`` of JWE."""
|
||||
if not algorithm or algorithm.algorithm_type != "JWE":
|
||||
raise ValueError(f"Invalid algorithm for JWE, {algorithm!r}")
|
||||
|
||||
if algorithm.algorithm_location == "alg":
|
||||
cls.ALG_REGISTRY[algorithm.name] = algorithm
|
||||
elif algorithm.algorithm_location == "enc":
|
||||
cls.ENC_REGISTRY[algorithm.name] = algorithm
|
||||
elif algorithm.algorithm_location == "zip":
|
||||
cls.ZIP_REGISTRY[algorithm.name] = algorithm
|
||||
|
||||
def serialize_compact(self, protected, payload, key, sender_key=None):
|
||||
"""Generate a JWE Compact Serialization.
|
||||
|
||||
The JWE Compact Serialization represents encrypted content as a compact,
|
||||
URL-safe string. This string is::
|
||||
|
||||
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
|
||||
BASE64URL(JWE Encrypted Key) || '.' ||
|
||||
BASE64URL(JWE Initialization Vector) || '.' ||
|
||||
BASE64URL(JWE Ciphertext) || '.' ||
|
||||
BASE64URL(JWE Authentication Tag)
|
||||
|
||||
Only one recipient is supported by the JWE Compact Serialization and
|
||||
it provides no syntax to represent JWE Shared Unprotected Header, JWE
|
||||
Per-Recipient Unprotected Header, or JWE AAD values.
|
||||
|
||||
:param protected: A dict of protected header
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param key: Public key used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE compact serialization as bytes
|
||||
"""
|
||||
# step 1: Prepare algorithms & key
|
||||
alg = self.get_header_alg(protected)
|
||||
enc = self.get_header_enc(protected)
|
||||
zip_alg = self.get_header_zip(protected)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(protected, alg)
|
||||
|
||||
key = prepare_key(alg, protected, key)
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
# self._post_validate_header(protected, algorithm)
|
||||
|
||||
# step 2: Generate a random Content Encryption Key (CEK)
|
||||
# use enc_alg.generate_cek() in scope of upcoming .wrap
|
||||
# or .generate_keys_and_prepare_headers call
|
||||
|
||||
# step 3: Encrypt the CEK with the recipient's public key
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Defer key agreement with key wrapping until
|
||||
# authentication tag is computed
|
||||
prep = alg.generate_keys_and_prepare_headers(enc, key, sender_key)
|
||||
epk = prep["epk"]
|
||||
cek = prep["cek"]
|
||||
protected.update(prep["header"])
|
||||
else:
|
||||
# In any other case:
|
||||
# Keep the normal steps order defined by RFC 7516
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
wrapped = alg.wrap(enc, protected, key, sender_key)
|
||||
else:
|
||||
wrapped = alg.wrap(enc, protected, key)
|
||||
cek = wrapped["cek"]
|
||||
ek = wrapped["ek"]
|
||||
if "header" in wrapped:
|
||||
protected.update(wrapped["header"])
|
||||
|
||||
# step 4: Generate a random JWE Initialization Vector
|
||||
iv = enc.generate_iv()
|
||||
|
||||
# step 5: Let the Additional Authenticated Data encryption parameter
|
||||
# be ASCII(BASE64URL(UTF8(JWE Protected Header)))
|
||||
protected_segment = json_b64encode(protected)
|
||||
aad = to_bytes(protected_segment, "ascii")
|
||||
|
||||
# step 6: compress message if required
|
||||
if zip_alg:
|
||||
msg = zip_alg.compress(to_bytes(payload))
|
||||
else:
|
||||
msg = to_bytes(payload)
|
||||
|
||||
# step 7: perform encryption
|
||||
ciphertext, tag = enc.encrypt(msg, aad, iv, cek)
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Perform key agreement with key wrapping deferred at step 3
|
||||
wrapped = alg.agree_upon_key_and_wrap_cek(
|
||||
enc, protected, key, sender_key, epk, cek, tag
|
||||
)
|
||||
ek = wrapped["ek"]
|
||||
|
||||
# step 8: build resulting message
|
||||
return b".".join(
|
||||
[
|
||||
protected_segment,
|
||||
urlsafe_b64encode(ek),
|
||||
urlsafe_b64encode(iv),
|
||||
urlsafe_b64encode(ciphertext),
|
||||
urlsafe_b64encode(tag),
|
||||
]
|
||||
)
|
||||
|
||||
def serialize_json(self, header_obj, payload, keys, sender_key=None): # noqa: C901
|
||||
"""Generate a JWE JSON Serialization (in fully general syntax).
|
||||
|
||||
The JWE JSON Serialization represents encrypted content as a JSON
|
||||
object. This representation is neither optimized for compactness nor
|
||||
URL safe.
|
||||
|
||||
The following members are defined for use in top-level JSON objects
|
||||
used for the fully general JWE JSON Serialization syntax:
|
||||
|
||||
protected
|
||||
The "protected" member MUST be present and contain the value
|
||||
BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected
|
||||
Header value is non-empty; otherwise, it MUST be absent. These
|
||||
Header Parameter values are integrity protected.
|
||||
|
||||
unprotected
|
||||
The "unprotected" member MUST be present and contain the value JWE
|
||||
Shared Unprotected Header when the JWE Shared Unprotected Header
|
||||
value is non-empty; otherwise, it MUST be absent. This value is
|
||||
represented as an unencoded JSON object, rather than as a string.
|
||||
These Header Parameter values are not integrity protected.
|
||||
|
||||
iv
|
||||
The "iv" member MUST be present and contain the value
|
||||
BASE64URL(JWE Initialization Vector) when the JWE Initialization
|
||||
Vector value is non-empty; otherwise, it MUST be absent.
|
||||
|
||||
aad
|
||||
The "aad" member MUST be present and contain the value
|
||||
BASE64URL(JWE AAD)) when the JWE AAD value is non-empty;
|
||||
otherwise, it MUST be absent. A JWE AAD value can be included to
|
||||
supply a base64url-encoded value to be integrity protected but not
|
||||
encrypted.
|
||||
|
||||
ciphertext
|
||||
The "ciphertext" member MUST be present and contain the value
|
||||
BASE64URL(JWE Ciphertext).
|
||||
|
||||
tag
|
||||
The "tag" member MUST be present and contain the value
|
||||
BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag
|
||||
value is non-empty; otherwise, it MUST be absent.
|
||||
|
||||
recipients
|
||||
The "recipients" member value MUST be an array of JSON objects.
|
||||
Each object contains information specific to a single recipient.
|
||||
This member MUST be present with exactly one array element per
|
||||
recipient, even if some or all of the array element values are the
|
||||
empty JSON object "{}" (which can happen when all Header Parameter
|
||||
values are shared between all recipients and when no encrypted key
|
||||
is used, such as when doing Direct Encryption).
|
||||
|
||||
The following members are defined for use in the JSON objects that
|
||||
are elements of the "recipients" array:
|
||||
|
||||
header
|
||||
The "header" member MUST be present and contain the value JWE Per-
|
||||
Recipient Unprotected Header when the JWE Per-Recipient
|
||||
Unprotected Header value is non-empty; otherwise, it MUST be
|
||||
absent. This value is represented as an unencoded JSON object,
|
||||
rather than as a string. These Header Parameter values are not
|
||||
integrity protected.
|
||||
|
||||
encrypted_key
|
||||
The "encrypted_key" member MUST be present and contain the value
|
||||
BASE64URL(JWE Encrypted Key) when the JWE Encrypted Key value is
|
||||
non-empty; otherwise, it MUST be absent.
|
||||
|
||||
This implementation assumes that "alg" and "enc" header fields are
|
||||
contained in the protected or shared unprotected header.
|
||||
|
||||
:param header_obj: A dict of headers (in addition optionally contains JWE AAD)
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param keys: Public keys (or a single public key) used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE JSON serialization (in fully general syntax) as dict
|
||||
|
||||
Example of `header_obj`::
|
||||
|
||||
{
|
||||
"protected": {
|
||||
"alg": "ECDH-1PU+A128KW",
|
||||
"enc": "A256CBC-HS512",
|
||||
"apu": "QWxpY2U",
|
||||
"apv": "Qm9iIGFuZCBDaGFybGll",
|
||||
},
|
||||
"unprotected": {"jku": "https://alice.example.com/keys.jwks"},
|
||||
"recipients": [
|
||||
{"header": {"kid": "bob-key-2"}},
|
||||
{"header": {"kid": "2021-05-06"}},
|
||||
],
|
||||
"aad": b"Authenticate me too.",
|
||||
}
|
||||
"""
|
||||
if not isinstance(keys, list): # single key
|
||||
keys = [keys]
|
||||
|
||||
if not keys:
|
||||
raise ValueError("No keys have been provided")
|
||||
|
||||
header_obj = deepcopy(header_obj)
|
||||
|
||||
shared_header = JWESharedHeader.from_dict(header_obj)
|
||||
|
||||
recipients = header_obj.get("recipients")
|
||||
if recipients is None:
|
||||
recipients = [{} for _ in keys]
|
||||
for i in range(len(recipients)):
|
||||
if recipients[i] is None:
|
||||
recipients[i] = {}
|
||||
if "header" not in recipients[i]:
|
||||
recipients[i]["header"] = {}
|
||||
|
||||
jwe_aad = header_obj.get("aad")
|
||||
|
||||
if len(keys) != len(recipients):
|
||||
raise ValueError(
|
||||
f"Count of recipient keys {len(keys)} does not equal to count of recipients {len(recipients)}"
|
||||
)
|
||||
|
||||
# step 1: Prepare algorithms & key
|
||||
alg = self.get_header_alg(shared_header)
|
||||
enc = self.get_header_enc(shared_header)
|
||||
zip_alg = self.get_header_zip(shared_header)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(shared_header, alg)
|
||||
for recipient in recipients:
|
||||
self._validate_private_headers(recipient["header"], alg)
|
||||
|
||||
for i in range(len(keys)):
|
||||
keys[i] = prepare_key(alg, recipients[i]["header"], keys[i])
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
# self._post_validate_header(protected, algorithm)
|
||||
|
||||
# step 2: Generate a random Content Encryption Key (CEK)
|
||||
# use enc_alg.generate_cek() in scope of upcoming .wrap
|
||||
# or .generate_keys_and_prepare_headers call
|
||||
|
||||
# step 3: Encrypt the CEK with the recipient's public key
|
||||
preset = alg.generate_preset(enc, keys[0])
|
||||
if "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = None
|
||||
if len(keys) > 1 and cek is None:
|
||||
raise InvalidAlgorithmForMultipleRecipientsMode(alg.name)
|
||||
if "header" in preset:
|
||||
shared_header.update_protected(preset["header"])
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Defer key agreement with key wrapping until authentication tag is computed
|
||||
epks = []
|
||||
for i in range(len(keys)):
|
||||
prep = alg.generate_keys_and_prepare_headers(
|
||||
enc, keys[i], sender_key, preset
|
||||
)
|
||||
if cek is None:
|
||||
cek = prep["cek"]
|
||||
epks.append(prep["epk"])
|
||||
recipients[i]["header"].update(prep["header"])
|
||||
else:
|
||||
# In any other case:
|
||||
# Keep the normal steps order defined by RFC 7516
|
||||
for i in range(len(keys)):
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
wrapped = alg.wrap(enc, shared_header, keys[i], sender_key, preset)
|
||||
else:
|
||||
wrapped = alg.wrap(enc, shared_header, keys[i], preset)
|
||||
if cek is None:
|
||||
cek = wrapped["cek"]
|
||||
recipients[i]["encrypted_key"] = wrapped["ek"]
|
||||
if "header" in wrapped:
|
||||
recipients[i]["header"].update(wrapped["header"])
|
||||
|
||||
# step 4: Generate a random JWE Initialization Vector
|
||||
iv = enc.generate_iv()
|
||||
|
||||
# step 5: Compute the Encoded Protected Header value
|
||||
# BASE64URL(UTF8(JWE Protected Header)). If the JWE Protected Header
|
||||
# is not present, let this value be the empty string.
|
||||
# Let the Additional Authenticated Data encryption parameter be
|
||||
# ASCII(Encoded Protected Header). However, if a JWE AAD value is
|
||||
# present, instead let the Additional Authenticated Data encryption
|
||||
# parameter be ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)).
|
||||
aad = (
|
||||
json_b64encode(shared_header.protected) if shared_header.protected else b""
|
||||
)
|
||||
if jwe_aad is not None:
|
||||
aad += b"." + urlsafe_b64encode(jwe_aad)
|
||||
aad = to_bytes(aad, "ascii")
|
||||
|
||||
# step 6: compress message if required
|
||||
if zip_alg:
|
||||
msg = zip_alg.compress(to_bytes(payload))
|
||||
else:
|
||||
msg = to_bytes(payload)
|
||||
|
||||
# step 7: perform encryption
|
||||
ciphertext, tag = enc.encrypt(msg, aad, iv, cek)
|
||||
|
||||
if (
|
||||
isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement)
|
||||
and alg.key_size is not None
|
||||
):
|
||||
# For a JWE algorithm with tag-aware key agreement in case key agreement
|
||||
# with key wrapping mode is used:
|
||||
# Perform key agreement with key wrapping deferred at step 3
|
||||
for i in range(len(keys)):
|
||||
wrapped = alg.agree_upon_key_and_wrap_cek(
|
||||
enc, shared_header, keys[i], sender_key, epks[i], cek, tag
|
||||
)
|
||||
recipients[i]["encrypted_key"] = wrapped["ek"]
|
||||
|
||||
# step 8: build resulting message
|
||||
obj = OrderedDict()
|
||||
|
||||
if shared_header.protected:
|
||||
obj["protected"] = to_unicode(json_b64encode(shared_header.protected))
|
||||
|
||||
if shared_header.unprotected:
|
||||
obj["unprotected"] = shared_header.unprotected
|
||||
|
||||
for recipient in recipients:
|
||||
if not recipient["header"]:
|
||||
del recipient["header"]
|
||||
recipient["encrypted_key"] = to_unicode(
|
||||
urlsafe_b64encode(recipient["encrypted_key"])
|
||||
)
|
||||
for member in set(recipient.keys()):
|
||||
if member not in {"header", "encrypted_key"}:
|
||||
del recipient[member]
|
||||
obj["recipients"] = recipients
|
||||
|
||||
if jwe_aad is not None:
|
||||
obj["aad"] = to_unicode(urlsafe_b64encode(jwe_aad))
|
||||
|
||||
obj["iv"] = to_unicode(urlsafe_b64encode(iv))
|
||||
|
||||
obj["ciphertext"] = to_unicode(urlsafe_b64encode(ciphertext))
|
||||
|
||||
obj["tag"] = to_unicode(urlsafe_b64encode(tag))
|
||||
|
||||
return obj
|
||||
|
||||
def serialize(self, header, payload, key, sender_key=None):
|
||||
"""Generate a JWE Serialization.
|
||||
|
||||
It will automatically generate a compact or JSON serialization depending
|
||||
on `header` argument. If `header` is a dict with "protected",
|
||||
"unprotected" and/or "recipients" keys, it will call `serialize_json`,
|
||||
otherwise it will call `serialize_compact`.
|
||||
|
||||
:param header: A dict of header(s)
|
||||
:param payload: Payload (bytes or a value convertible to bytes)
|
||||
:param key: Public key(s) used to encrypt payload
|
||||
:param sender_key: Sender's private key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: JWE compact serialization as bytes or
|
||||
JWE JSON serialization as dict
|
||||
"""
|
||||
if "protected" in header or "unprotected" in header or "recipients" in header:
|
||||
return self.serialize_json(header, payload, key, sender_key)
|
||||
|
||||
return self.serialize_compact(header, payload, key, sender_key)
|
||||
|
||||
def deserialize_compact(self, s, key, decode=None, sender_key=None):
|
||||
"""Extract JWE Compact Serialization.
|
||||
|
||||
:param s: JWE Compact Serialization as bytes
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys where `header` value is
|
||||
a dict containing protected header fields
|
||||
"""
|
||||
try:
|
||||
s = to_bytes(s)
|
||||
protected_s, ek_s, iv_s, ciphertext_s, tag_s = s.rsplit(b".")
|
||||
except ValueError as exc:
|
||||
raise DecodeError("Not enough segments") from exc
|
||||
|
||||
protected = extract_header(protected_s, DecodeError)
|
||||
ek = extract_segment(ek_s, DecodeError, "encryption key")
|
||||
iv = extract_segment(iv_s, DecodeError, "initialization vector")
|
||||
ciphertext = extract_segment(ciphertext_s, DecodeError, "ciphertext")
|
||||
tag = extract_segment(tag_s, DecodeError, "authentication tag")
|
||||
|
||||
alg = self.get_header_alg(protected)
|
||||
enc = self.get_header_enc(protected)
|
||||
zip_alg = self.get_header_zip(protected)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(protected, alg)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
# Ignore separately provided kid, extract essentially key only
|
||||
key = key[1]
|
||||
|
||||
key = prepare_key(alg, protected, key)
|
||||
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
# For a JWE algorithm with tag-aware key agreement:
|
||||
if alg.key_size is not None:
|
||||
# In case key agreement with key wrapping mode is used:
|
||||
# Provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key, sender_key, tag)
|
||||
else:
|
||||
# Otherwise, don't provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key, sender_key)
|
||||
else:
|
||||
# For any other JWE algorithm:
|
||||
# Don't provide authentication tag to .unwrap method
|
||||
cek = alg.unwrap(enc, ek, protected, key)
|
||||
|
||||
aad = to_bytes(protected_s, "ascii")
|
||||
msg = enc.decrypt(ciphertext, aad, iv, tag, cek)
|
||||
|
||||
if zip_alg:
|
||||
payload = zip_alg.decompress(to_bytes(msg))
|
||||
else:
|
||||
payload = msg
|
||||
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
return {"header": protected, "payload": payload}
|
||||
|
||||
def deserialize_json(self, obj, key, decode=None, sender_key=None): # noqa: C901
|
||||
"""Extract JWE JSON Serialization.
|
||||
|
||||
:param obj: JWE JSON Serialization as dict or str
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys where `header` value is
|
||||
a dict containing `protected`, `unprotected`, `recipients` and/or
|
||||
`aad` keys
|
||||
"""
|
||||
obj = ensure_dict(obj, "JWE")
|
||||
obj = deepcopy(obj)
|
||||
|
||||
if "protected" in obj:
|
||||
protected = extract_header(to_bytes(obj["protected"]), DecodeError)
|
||||
else:
|
||||
protected = None
|
||||
|
||||
unprotected = obj.get("unprotected")
|
||||
|
||||
recipients = obj["recipients"]
|
||||
for recipient in recipients:
|
||||
if "header" not in recipient:
|
||||
recipient["header"] = {}
|
||||
recipient["encrypted_key"] = extract_segment(
|
||||
to_bytes(recipient["encrypted_key"]), DecodeError, "encrypted key"
|
||||
)
|
||||
|
||||
if "aad" in obj:
|
||||
jwe_aad = extract_segment(to_bytes(obj["aad"]), DecodeError, "JWE AAD")
|
||||
else:
|
||||
jwe_aad = None
|
||||
|
||||
iv = extract_segment(to_bytes(obj["iv"]), DecodeError, "initialization vector")
|
||||
|
||||
ciphertext = extract_segment(
|
||||
to_bytes(obj["ciphertext"]), DecodeError, "ciphertext"
|
||||
)
|
||||
|
||||
tag = extract_segment(to_bytes(obj["tag"]), DecodeError, "authentication tag")
|
||||
|
||||
shared_header = JWESharedHeader(protected, unprotected)
|
||||
|
||||
alg = self.get_header_alg(shared_header)
|
||||
enc = self.get_header_enc(shared_header)
|
||||
zip_alg = self.get_header_zip(shared_header)
|
||||
|
||||
self._validate_sender_key(sender_key, alg)
|
||||
self._validate_private_headers(shared_header, alg)
|
||||
for recipient in recipients:
|
||||
self._validate_private_headers(recipient["header"], alg)
|
||||
|
||||
kid = None
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
# Extract separately provided kid and essentially key
|
||||
kid = key[0]
|
||||
key = key[1]
|
||||
|
||||
key = alg.prepare_key(key)
|
||||
|
||||
if kid is None:
|
||||
# If kid has not been provided separately, try to get it from key itself
|
||||
kid = key.kid
|
||||
|
||||
if sender_key is not None:
|
||||
sender_key = alg.prepare_key(sender_key)
|
||||
|
||||
def _unwrap_with_sender_key_and_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key, sender_key, tag)
|
||||
|
||||
def _unwrap_with_sender_key_and_without_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key, sender_key)
|
||||
|
||||
def _unwrap_without_sender_key_and_tag(ek, header):
|
||||
return alg.unwrap(enc, ek, header, key)
|
||||
|
||||
def _unwrap_for_matching_recipient(unwrap_func):
|
||||
if kid is not None:
|
||||
for recipient in recipients:
|
||||
if recipient["header"].get("kid") == kid:
|
||||
header = JWEHeader(protected, unprotected, recipient["header"])
|
||||
return unwrap_func(recipient["encrypted_key"], header)
|
||||
|
||||
# Since no explicit match has been found, iterate over all the recipients
|
||||
error = None
|
||||
for recipient in recipients:
|
||||
header = JWEHeader(protected, unprotected, recipient["header"])
|
||||
try:
|
||||
return unwrap_func(recipient["encrypted_key"], header)
|
||||
except Exception as e:
|
||||
error = e
|
||||
else:
|
||||
if error is None:
|
||||
raise KeyMismatchError()
|
||||
else:
|
||||
raise error
|
||||
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
# For a JWE algorithm with tag-aware key agreement:
|
||||
if alg.key_size is not None:
|
||||
# In case key agreement with key wrapping mode is used:
|
||||
# Provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(_unwrap_with_sender_key_and_tag)
|
||||
else:
|
||||
# Otherwise, don't provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(
|
||||
_unwrap_with_sender_key_and_without_tag
|
||||
)
|
||||
else:
|
||||
# For any other JWE algorithm:
|
||||
# Don't provide authentication tag to .unwrap method
|
||||
cek = _unwrap_for_matching_recipient(_unwrap_without_sender_key_and_tag)
|
||||
|
||||
aad = to_bytes(obj.get("protected", ""))
|
||||
if "aad" in obj:
|
||||
aad += b"." + to_bytes(obj["aad"])
|
||||
aad = to_bytes(aad, "ascii")
|
||||
|
||||
msg = enc.decrypt(ciphertext, aad, iv, tag, cek)
|
||||
|
||||
if zip_alg:
|
||||
payload = zip_alg.decompress(to_bytes(msg))
|
||||
else:
|
||||
payload = msg
|
||||
|
||||
if decode:
|
||||
payload = decode(payload)
|
||||
|
||||
for recipient in recipients:
|
||||
if not recipient["header"]:
|
||||
del recipient["header"]
|
||||
for member in set(recipient.keys()):
|
||||
if member != "header":
|
||||
del recipient[member]
|
||||
|
||||
header = {}
|
||||
if protected:
|
||||
header["protected"] = protected
|
||||
if unprotected:
|
||||
header["unprotected"] = unprotected
|
||||
header["recipients"] = recipients
|
||||
if jwe_aad is not None:
|
||||
header["aad"] = jwe_aad
|
||||
|
||||
return {"header": header, "payload": payload}
|
||||
|
||||
def deserialize(self, obj, key, decode=None, sender_key=None):
|
||||
"""Extract a JWE Serialization.
|
||||
|
||||
It supports both compact and JSON serialization.
|
||||
|
||||
:param obj: JWE compact serialization as bytes or
|
||||
JWE JSON serialization as dict or str
|
||||
:param key: Private key used to decrypt payload
|
||||
(optionally can be a tuple of kid and essentially key)
|
||||
:param decode: Function to decode payload data
|
||||
:param sender_key: Sender's public key in case
|
||||
JWEAlgorithmWithTagAwareKeyAgreement is used
|
||||
:return: dict with `header` and `payload` keys
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return self.deserialize_json(obj, key, decode, sender_key)
|
||||
|
||||
obj = to_bytes(obj)
|
||||
if obj.startswith(b"{") and obj.endswith(b"}"):
|
||||
return self.deserialize_json(obj, key, decode, sender_key)
|
||||
|
||||
return self.deserialize_compact(obj, key, decode, sender_key)
|
||||
|
||||
@staticmethod
|
||||
def parse_json(obj):
|
||||
"""Parse JWE JSON Serialization.
|
||||
|
||||
:param obj: JWE JSON Serialization as str or dict
|
||||
:return: Parsed JWE JSON Serialization as dict if `obj` is an str,
|
||||
or `obj` as is if `obj` is already a dict
|
||||
"""
|
||||
return ensure_dict(obj, "JWE")
|
||||
|
||||
def get_header_alg(self, header):
|
||||
if "alg" not in header:
|
||||
raise MissingAlgorithmError()
|
||||
|
||||
alg = header["alg"]
|
||||
if alg not in self.ALG_REGISTRY:
|
||||
raise UnsupportedAlgorithmError()
|
||||
|
||||
instance = self.ALG_REGISTRY[alg]
|
||||
|
||||
# use all ALG_REGISTRY algorithms
|
||||
if self._algorithms is None:
|
||||
# do not use deprecated algorithms
|
||||
if instance.deprecated:
|
||||
raise UnsupportedAlgorithmError()
|
||||
elif alg not in self._algorithms:
|
||||
raise UnsupportedAlgorithmError()
|
||||
return instance
|
||||
|
||||
def get_header_enc(self, header):
|
||||
if "enc" not in header:
|
||||
raise MissingEncryptionAlgorithmError()
|
||||
enc = header["enc"]
|
||||
if self._algorithms is not None and enc not in self._algorithms:
|
||||
raise UnsupportedEncryptionAlgorithmError()
|
||||
if enc not in self.ENC_REGISTRY:
|
||||
raise UnsupportedEncryptionAlgorithmError()
|
||||
return self.ENC_REGISTRY[enc]
|
||||
|
||||
def get_header_zip(self, header):
|
||||
if "zip" in header:
|
||||
z = header["zip"]
|
||||
if self._algorithms is not None and z not in self._algorithms:
|
||||
raise UnsupportedCompressionAlgorithmError()
|
||||
if z not in self.ZIP_REGISTRY:
|
||||
raise UnsupportedCompressionAlgorithmError()
|
||||
return self.ZIP_REGISTRY[z]
|
||||
|
||||
def _validate_sender_key(self, sender_key, alg):
|
||||
if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement):
|
||||
if sender_key is None:
|
||||
raise ValueError(
|
||||
f"{alg.name} algorithm requires sender_key but passed sender_key value is None"
|
||||
)
|
||||
else:
|
||||
if sender_key is not None:
|
||||
raise ValueError(
|
||||
f"{alg.name} algorithm does not use sender_key but passed sender_key value is not None"
|
||||
)
|
||||
|
||||
def _validate_private_headers(self, header, alg):
|
||||
# only validate private headers when developers set
|
||||
# private headers explicitly
|
||||
if self._private_headers is None:
|
||||
return
|
||||
|
||||
names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy()
|
||||
names = names.union(self._private_headers)
|
||||
|
||||
if alg.EXTRA_HEADERS:
|
||||
names = names.union(alg.EXTRA_HEADERS)
|
||||
|
||||
for k in header:
|
||||
if k not in names:
|
||||
raise InvalidHeaderParameterNameError(k)
|
||||
|
||||
|
||||
def prepare_key(alg, header, key):
|
||||
if callable(key):
|
||||
key = key(header, None)
|
||||
return alg.prepare_key(key)
|
||||
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class JWEAlgorithmBase(metaclass=ABCMeta): # noqa: B024
|
||||
"""Base interface for all JWE algorithms."""
|
||||
|
||||
EXTRA_HEADERS = None
|
||||
|
||||
name = None
|
||||
description = None
|
||||
deprecated = False
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "alg"
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEAlgorithm(JWEAlgorithmBase, metaclass=ABCMeta):
|
||||
"""Interface for JWE algorithm conforming to RFC7518.
|
||||
JWA specification (RFC7518) SHOULD implement the algorithms for JWE
|
||||
with this base implementation.
|
||||
"""
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEAlgorithmWithTagAwareKeyAgreement(JWEAlgorithmBase, metaclass=ABCMeta):
|
||||
"""Interface for JWE algorithm with tag-aware key agreement (in key agreement
|
||||
with key wrapping mode).
|
||||
ECDH-1PU is an example of such an algorithm.
|
||||
"""
|
||||
|
||||
def generate_keys_and_prepare_headers(self, enc_alg, key, sender_key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def agree_upon_key_and_wrap_cek(
|
||||
self, enc_alg, headers, key, sender_key, epk, cek, tag
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
def wrap(self, enc_alg, headers, key, sender_key, preset=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key, sender_key, tag=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEEncAlgorithm:
|
||||
name = None
|
||||
description = None
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "enc"
|
||||
|
||||
IV_SIZE = None
|
||||
CEK_SIZE = None
|
||||
|
||||
def generate_cek(self):
|
||||
return os.urandom(self.CEK_SIZE // 8)
|
||||
|
||||
def generate_iv(self):
|
||||
return os.urandom(self.IV_SIZE // 8)
|
||||
|
||||
def check_iv(self, iv):
|
||||
if len(iv) * 8 != self.IV_SIZE:
|
||||
raise ValueError('Invalid "iv" size')
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Encrypt the given "msg" text.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, tag)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Decrypt the given cipher text.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWEZipAlgorithm:
|
||||
name = None
|
||||
description = None
|
||||
algorithm_type = "JWE"
|
||||
algorithm_location = "zip"
|
||||
|
||||
def compress(self, s):
|
||||
raise NotImplementedError
|
||||
|
||||
def decompress(self, s):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class JWESharedHeader(dict):
|
||||
"""Shared header object for JWE.
|
||||
|
||||
Combines protected header and shared unprotected header together.
|
||||
"""
|
||||
|
||||
def __init__(self, protected, unprotected):
|
||||
obj = {}
|
||||
if unprotected:
|
||||
obj.update(unprotected)
|
||||
if protected:
|
||||
obj.update(protected)
|
||||
super().__init__(obj)
|
||||
self.protected = protected if protected else {}
|
||||
self.unprotected = unprotected if unprotected else {}
|
||||
|
||||
def update_protected(self, addition):
|
||||
self.update(addition)
|
||||
self.protected.update(addition)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj):
|
||||
if isinstance(obj, cls):
|
||||
return obj
|
||||
return cls(obj.get("protected"), obj.get("unprotected"))
|
||||
|
||||
|
||||
class JWEHeader(dict):
|
||||
"""Header object for JWE.
|
||||
|
||||
Combines protected header, shared unprotected header
|
||||
and specific recipient's unprotected header together.
|
||||
"""
|
||||
|
||||
def __init__(self, protected, unprotected, header):
|
||||
obj = {}
|
||||
if unprotected:
|
||||
obj.update(unprotected)
|
||||
if header:
|
||||
obj.update(header)
|
||||
if protected:
|
||||
obj.update(protected)
|
||||
super().__init__(obj)
|
||||
self.protected = protected if protected else {}
|
||||
self.unprotected = unprotected if unprotected else {}
|
||||
self.header = header if header else {}
|
||||
@@ -0,0 +1,16 @@
|
||||
"""authlib.jose.rfc7517.
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
JSON Web Key (JWK).
|
||||
|
||||
https://tools.ietf.org/html/rfc7517
|
||||
"""
|
||||
|
||||
from ._cryptography_key import load_pem_key
|
||||
from .asymmetric_key import AsymmetricKey
|
||||
from .base_key import Key
|
||||
from .jwk import JsonWebKey
|
||||
from .key_set import KeySet
|
||||
|
||||
__all__ = ["Key", "AsymmetricKey", "KeySet", "JsonWebKey", "load_pem_key"]
|
||||
@@ -0,0 +1,35 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
from cryptography.hazmat.primitives.serialization import load_ssh_public_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
|
||||
|
||||
def load_pem_key(raw, ssh_type=None, key_type=None, password=None):
|
||||
raw = to_bytes(raw)
|
||||
|
||||
if ssh_type and raw.startswith(ssh_type):
|
||||
return load_ssh_public_key(raw, backend=default_backend())
|
||||
|
||||
if key_type == "public":
|
||||
return load_pem_public_key(raw, backend=default_backend())
|
||||
|
||||
if key_type == "private" or password is not None:
|
||||
return load_pem_private_key(raw, password=password, backend=default_backend())
|
||||
|
||||
if b"PUBLIC" in raw:
|
||||
return load_pem_public_key(raw, backend=default_backend())
|
||||
|
||||
if b"PRIVATE" in raw:
|
||||
return load_pem_private_key(raw, password=password, backend=default_backend())
|
||||
|
||||
if b"CERTIFICATE" in raw:
|
||||
cert = load_pem_x509_certificate(raw, default_backend())
|
||||
return cert.public_key()
|
||||
|
||||
try:
|
||||
return load_pem_private_key(raw, password=password, backend=default_backend())
|
||||
except ValueError:
|
||||
return load_pem_public_key(raw, backend=default_backend())
|
||||
@@ -0,0 +1,196 @@
|
||||
from cryptography.hazmat.primitives.serialization import BestAvailableEncryption
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.hazmat.primitives.serialization import NoEncryption
|
||||
from cryptography.hazmat.primitives.serialization import PrivateFormat
|
||||
from cryptography.hazmat.primitives.serialization import PublicFormat
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
|
||||
from ._cryptography_key import load_pem_key
|
||||
from .base_key import Key
|
||||
|
||||
|
||||
class AsymmetricKey(Key):
|
||||
"""This is the base class for a JSON Web Key."""
|
||||
|
||||
PUBLIC_KEY_FIELDS = []
|
||||
PRIVATE_KEY_FIELDS = []
|
||||
PRIVATE_KEY_CLS = bytes
|
||||
PUBLIC_KEY_CLS = bytes
|
||||
SSH_PUBLIC_PREFIX = b""
|
||||
|
||||
def __init__(self, private_key=None, public_key=None, options=None):
|
||||
super().__init__(options)
|
||||
self.private_key = private_key
|
||||
self.public_key = public_key
|
||||
|
||||
@property
|
||||
def public_only(self):
|
||||
if self.private_key:
|
||||
return False
|
||||
if "d" in self.tokens:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_op_key(self, operation):
|
||||
"""Get the raw key for the given key_op. This method will also
|
||||
check if the given key_op is supported by this key.
|
||||
|
||||
:param operation: key operation value, such as "sign", "encrypt".
|
||||
:return: raw key
|
||||
"""
|
||||
self.check_key_op(operation)
|
||||
if operation in self.PUBLIC_KEY_OPS:
|
||||
return self.get_public_key()
|
||||
return self.get_private_key()
|
||||
|
||||
def get_public_key(self):
|
||||
if self.public_key:
|
||||
return self.public_key
|
||||
|
||||
private_key = self.get_private_key()
|
||||
if private_key:
|
||||
return private_key.public_key()
|
||||
|
||||
return self.public_key
|
||||
|
||||
def get_private_key(self):
|
||||
if self.private_key:
|
||||
return self.private_key
|
||||
|
||||
if self.tokens:
|
||||
self.load_raw_key()
|
||||
return self.private_key
|
||||
|
||||
def load_raw_key(self):
|
||||
if "d" in self.tokens:
|
||||
self.private_key = self.load_private_key()
|
||||
else:
|
||||
self.public_key = self.load_public_key()
|
||||
|
||||
def load_dict_key(self):
|
||||
if self.private_key:
|
||||
self._dict_data.update(self.dumps_private_key())
|
||||
else:
|
||||
self._dict_data.update(self.dumps_public_key())
|
||||
|
||||
def dumps_private_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def dumps_public_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def load_private_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def load_public_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def as_dict(self, is_private=False, **params):
|
||||
"""Represent this key as a dict of the JSON Web Key."""
|
||||
tokens = self.tokens
|
||||
if is_private and "d" not in tokens:
|
||||
raise ValueError("This is a public key")
|
||||
|
||||
kid = tokens.get("kid")
|
||||
if "d" in tokens and not is_private:
|
||||
# filter out private fields
|
||||
tokens = {k: tokens[k] for k in tokens if k in self.PUBLIC_KEY_FIELDS}
|
||||
tokens["kty"] = self.kty
|
||||
if kid:
|
||||
tokens["kid"] = kid
|
||||
|
||||
if not kid:
|
||||
tokens["kid"] = self.thumbprint()
|
||||
|
||||
tokens.update(params)
|
||||
return tokens
|
||||
|
||||
def as_key(self, is_private=False):
|
||||
"""Represent this key as raw key."""
|
||||
if is_private:
|
||||
return self.get_private_key()
|
||||
return self.get_public_key()
|
||||
|
||||
def as_bytes(self, encoding=None, is_private=False, password=None):
|
||||
"""Export key into PEM/DER format bytes.
|
||||
|
||||
:param encoding: "PEM" or "DER"
|
||||
:param is_private: export private key or public key
|
||||
:param password: encrypt private key with password
|
||||
:return: bytes
|
||||
"""
|
||||
if encoding is None or encoding == "PEM":
|
||||
encoding = Encoding.PEM
|
||||
elif encoding == "DER":
|
||||
encoding = Encoding.DER
|
||||
else:
|
||||
raise ValueError(f"Invalid encoding: {encoding!r}")
|
||||
|
||||
raw_key = self.as_key(is_private)
|
||||
if is_private:
|
||||
if not raw_key:
|
||||
raise ValueError("This is a public key")
|
||||
if password is None:
|
||||
encryption_algorithm = NoEncryption()
|
||||
else:
|
||||
encryption_algorithm = BestAvailableEncryption(to_bytes(password))
|
||||
return raw_key.private_bytes(
|
||||
encoding=encoding,
|
||||
format=PrivateFormat.PKCS8,
|
||||
encryption_algorithm=encryption_algorithm,
|
||||
)
|
||||
return raw_key.public_bytes(
|
||||
encoding=encoding,
|
||||
format=PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
|
||||
def as_pem(self, is_private=False, password=None):
|
||||
return self.as_bytes(is_private=is_private, password=password)
|
||||
|
||||
def as_der(self, is_private=False, password=None):
|
||||
return self.as_bytes(encoding="DER", is_private=is_private, password=password)
|
||||
|
||||
@classmethod
|
||||
def import_dict_key(cls, raw, options=None):
|
||||
cls.check_required_fields(raw)
|
||||
key = cls(options=options)
|
||||
key._dict_data = raw
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def import_key(cls, raw, options=None):
|
||||
if isinstance(raw, cls):
|
||||
if options is not None:
|
||||
raw.options.update(options)
|
||||
return raw
|
||||
|
||||
if isinstance(raw, cls.PUBLIC_KEY_CLS):
|
||||
key = cls(public_key=raw, options=options)
|
||||
elif isinstance(raw, cls.PRIVATE_KEY_CLS):
|
||||
key = cls(private_key=raw, options=options)
|
||||
elif isinstance(raw, dict):
|
||||
key = cls.import_dict_key(raw, options)
|
||||
else:
|
||||
if options is not None:
|
||||
password = options.pop("password", None)
|
||||
else:
|
||||
password = None
|
||||
raw_key = load_pem_key(raw, cls.SSH_PUBLIC_PREFIX, password=password)
|
||||
if isinstance(raw_key, cls.PUBLIC_KEY_CLS):
|
||||
key = cls(public_key=raw_key, options=options)
|
||||
elif isinstance(raw_key, cls.PRIVATE_KEY_CLS):
|
||||
key = cls(private_key=raw_key, options=options)
|
||||
else:
|
||||
raise ValueError("Invalid data for importing key")
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def validate_raw_key(cls, key):
|
||||
return isinstance(key, cls.PUBLIC_KEY_CLS) or isinstance(
|
||||
key, cls.PRIVATE_KEY_CLS
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, crv_or_size, options=None, is_private=False):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,120 @@
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
from authlib.common.encoding import json_dumps
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
|
||||
from ..errors import InvalidUseError
|
||||
|
||||
|
||||
class Key:
|
||||
"""This is the base class for a JSON Web Key."""
|
||||
|
||||
kty = "_"
|
||||
|
||||
ALLOWED_PARAMS = ["use", "key_ops", "alg", "kid", "x5u", "x5c", "x5t", "x5t#S256"]
|
||||
|
||||
PRIVATE_KEY_OPS = [
|
||||
"sign",
|
||||
"decrypt",
|
||||
"unwrapKey",
|
||||
]
|
||||
PUBLIC_KEY_OPS = [
|
||||
"verify",
|
||||
"encrypt",
|
||||
"wrapKey",
|
||||
]
|
||||
|
||||
REQUIRED_JSON_FIELDS = []
|
||||
|
||||
def __init__(self, options=None):
|
||||
self.options = options or {}
|
||||
self._dict_data = {}
|
||||
|
||||
@property
|
||||
def tokens(self):
|
||||
if not self._dict_data:
|
||||
self.load_dict_key()
|
||||
|
||||
rv = dict(self._dict_data)
|
||||
rv["kty"] = self.kty
|
||||
for k in self.ALLOWED_PARAMS:
|
||||
if k not in rv and k in self.options:
|
||||
rv[k] = self.options[k]
|
||||
return rv
|
||||
|
||||
@property
|
||||
def kid(self):
|
||||
return self.tokens.get("kid")
|
||||
|
||||
def keys(self):
|
||||
return self.tokens.keys()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.tokens[item]
|
||||
|
||||
@property
|
||||
def public_only(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def load_raw_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def load_dict_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_key_op(self, operation):
|
||||
"""Check if the given key_op is supported by this key.
|
||||
|
||||
:param operation: key operation value, such as "sign", "encrypt".
|
||||
:raise: ValueError
|
||||
"""
|
||||
key_ops = self.tokens.get("key_ops")
|
||||
if key_ops is not None and operation not in key_ops:
|
||||
raise ValueError(f'Unsupported key_op "{operation}"')
|
||||
|
||||
if operation in self.PRIVATE_KEY_OPS and self.public_only:
|
||||
raise ValueError(f'Invalid key_op "{operation}" for public key')
|
||||
|
||||
use = self.tokens.get("use")
|
||||
if use:
|
||||
if operation in ["sign", "verify"]:
|
||||
if use != "sig":
|
||||
raise InvalidUseError()
|
||||
elif operation in ["decrypt", "encrypt", "wrapKey", "unwrapKey"]:
|
||||
if use != "enc":
|
||||
raise InvalidUseError()
|
||||
|
||||
def as_dict(self, is_private=False, **params):
|
||||
raise NotImplementedError()
|
||||
|
||||
def as_json(self, is_private=False, **params):
|
||||
"""Represent this key as a JSON string."""
|
||||
obj = self.as_dict(is_private, **params)
|
||||
return json_dumps(obj)
|
||||
|
||||
def thumbprint(self):
|
||||
"""Implementation of RFC7638 JSON Web Key (JWK) Thumbprint."""
|
||||
fields = list(self.REQUIRED_JSON_FIELDS)
|
||||
fields.append("kty")
|
||||
fields.sort()
|
||||
data = OrderedDict()
|
||||
|
||||
for k in fields:
|
||||
data[k] = self.tokens[k]
|
||||
|
||||
json_data = json_dumps(data)
|
||||
digest_data = hashlib.sha256(to_bytes(json_data)).digest()
|
||||
return to_unicode(urlsafe_b64encode(digest_data))
|
||||
|
||||
@classmethod
|
||||
def check_required_fields(cls, data):
|
||||
for k in cls.REQUIRED_JSON_FIELDS:
|
||||
if k not in data:
|
||||
raise ValueError(f'Missing required field: "{k}"')
|
||||
|
||||
@classmethod
|
||||
def validate_raw_key(cls, key):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,64 @@
|
||||
from authlib.common.encoding import json_loads
|
||||
|
||||
from ._cryptography_key import load_pem_key
|
||||
from .key_set import KeySet
|
||||
|
||||
|
||||
class JsonWebKey:
|
||||
JWK_KEY_CLS = {}
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, kty, crv_or_size, options=None, is_private=False):
|
||||
"""Generate a Key with the given key type, curve name or bit size.
|
||||
|
||||
:param kty: string of ``oct``, ``RSA``, ``EC``, ``OKP``
|
||||
:param crv_or_size: curve name or bit size
|
||||
:param options: a dict of other options for Key
|
||||
:param is_private: create a private key or public key
|
||||
:return: Key instance
|
||||
"""
|
||||
key_cls = cls.JWK_KEY_CLS[kty]
|
||||
return key_cls.generate_key(crv_or_size, options, is_private)
|
||||
|
||||
@classmethod
|
||||
def import_key(cls, raw, options=None):
|
||||
"""Import a Key from bytes, string, PEM or dict.
|
||||
|
||||
:return: Key instance
|
||||
"""
|
||||
kty = None
|
||||
if options is not None:
|
||||
kty = options.get("kty")
|
||||
|
||||
if kty is None and isinstance(raw, dict):
|
||||
kty = raw.get("kty")
|
||||
|
||||
if kty is None:
|
||||
raw_key = load_pem_key(raw)
|
||||
for _kty in cls.JWK_KEY_CLS:
|
||||
key_cls = cls.JWK_KEY_CLS[_kty]
|
||||
if key_cls.validate_raw_key(raw_key):
|
||||
return key_cls.import_key(raw_key, options)
|
||||
|
||||
key_cls = cls.JWK_KEY_CLS[kty]
|
||||
return key_cls.import_key(raw, options)
|
||||
|
||||
@classmethod
|
||||
def import_key_set(cls, raw):
|
||||
"""Import KeySet from string, dict or a list of keys.
|
||||
|
||||
:return: KeySet instance
|
||||
"""
|
||||
raw = _transform_raw_key(raw)
|
||||
if isinstance(raw, dict) and "keys" in raw:
|
||||
keys = raw.get("keys")
|
||||
return KeySet([cls.import_key(k) for k in keys])
|
||||
raise ValueError("Invalid key set format")
|
||||
|
||||
|
||||
def _transform_raw_key(raw):
|
||||
if isinstance(raw, str) and raw.startswith("{") and raw.endswith("}"):
|
||||
return json_loads(raw)
|
||||
elif isinstance(raw, (tuple, list)):
|
||||
return {"keys": raw}
|
||||
return raw
|
||||
@@ -0,0 +1,53 @@
|
||||
from authlib.common.encoding import json_dumps
|
||||
|
||||
|
||||
class KeySet:
|
||||
"""This class represents a JSON Web Key Set."""
|
||||
|
||||
def __init__(self, keys):
|
||||
self.keys = keys
|
||||
|
||||
def as_dict(self, is_private=False, **params):
|
||||
"""Represent this key as a dict of the JSON Web Key Set."""
|
||||
return {"keys": [k.as_dict(is_private, **params) for k in self.keys]}
|
||||
|
||||
def as_json(self, is_private=False, **params):
|
||||
"""Represent this key set as a JSON string."""
|
||||
obj = self.as_dict(is_private, **params)
|
||||
return json_dumps(obj)
|
||||
|
||||
def find_by_kid(self, kid, **params):
|
||||
"""Find the key matches the given kid value.
|
||||
|
||||
:param kid: A string of kid
|
||||
:return: Key instance
|
||||
:raise: ValueError
|
||||
"""
|
||||
# Proposed fix, feel free to do something else but the idea is that we take the only key
|
||||
# of the set if no kid is specified
|
||||
if kid is None and len(self.keys) == 1:
|
||||
return self.keys[0]
|
||||
|
||||
keys = [key for key in self.keys if key.kid == kid]
|
||||
if params:
|
||||
keys = list(_filter_keys_by_params(keys, **params))
|
||||
|
||||
if keys:
|
||||
return keys[0]
|
||||
raise ValueError("Key not found")
|
||||
|
||||
|
||||
def _filter_keys_by_params(keys, **params):
|
||||
_use = params.get("use")
|
||||
_alg = params.get("alg")
|
||||
|
||||
for key in keys:
|
||||
designed_use = key.tokens.get("use")
|
||||
if designed_use and _use and designed_use != _use:
|
||||
continue
|
||||
|
||||
designed_alg = key.tokens.get("alg")
|
||||
if designed_alg and _alg and designed_alg != _alg:
|
||||
continue
|
||||
|
||||
yield key
|
||||
@@ -0,0 +1,39 @@
|
||||
from .ec_key import ECKey
|
||||
from .jwe_algs import JWE_ALG_ALGORITHMS
|
||||
from .jwe_algs import AESAlgorithm
|
||||
from .jwe_algs import ECDHESAlgorithm
|
||||
from .jwe_algs import u32be_len_input
|
||||
from .jwe_encs import JWE_ENC_ALGORITHMS
|
||||
from .jwe_encs import CBCHS2EncAlgorithm
|
||||
from .jwe_zips import DeflateZipAlgorithm
|
||||
from .jws_algs import JWS_ALGORITHMS
|
||||
from .oct_key import OctKey
|
||||
from .rsa_key import RSAKey
|
||||
|
||||
|
||||
def register_jws_rfc7518(cls):
|
||||
for algorithm in JWS_ALGORITHMS:
|
||||
cls.register_algorithm(algorithm)
|
||||
|
||||
|
||||
def register_jwe_rfc7518(cls):
|
||||
for algorithm in JWE_ALG_ALGORITHMS:
|
||||
cls.register_algorithm(algorithm)
|
||||
|
||||
for algorithm in JWE_ENC_ALGORITHMS:
|
||||
cls.register_algorithm(algorithm)
|
||||
|
||||
cls.register_algorithm(DeflateZipAlgorithm())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"register_jws_rfc7518",
|
||||
"register_jwe_rfc7518",
|
||||
"OctKey",
|
||||
"RSAKey",
|
||||
"ECKey",
|
||||
"u32be_len_input",
|
||||
"AESAlgorithm",
|
||||
"ECDHESAlgorithm",
|
||||
"CBCHS2EncAlgorithm",
|
||||
]
|
||||
@@ -0,0 +1,108 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP256K1
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP384R1
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP521R1
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
EllipticCurvePrivateKeyWithSerialization,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateNumbers
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers
|
||||
|
||||
from authlib.common.encoding import base64_to_int
|
||||
from authlib.common.encoding import int_to_base64
|
||||
|
||||
from ..rfc7517 import AsymmetricKey
|
||||
|
||||
|
||||
class ECKey(AsymmetricKey):
|
||||
"""Key class of the ``EC`` key type."""
|
||||
|
||||
kty = "EC"
|
||||
DSS_CURVES = {
|
||||
"P-256": SECP256R1,
|
||||
"P-384": SECP384R1,
|
||||
"P-521": SECP521R1,
|
||||
# https://tools.ietf.org/html/rfc8812#section-3.1
|
||||
"secp256k1": SECP256K1,
|
||||
}
|
||||
CURVES_DSS = {
|
||||
SECP256R1.name: "P-256",
|
||||
SECP384R1.name: "P-384",
|
||||
SECP521R1.name: "P-521",
|
||||
SECP256K1.name: "secp256k1",
|
||||
}
|
||||
REQUIRED_JSON_FIELDS = ["crv", "x", "y"]
|
||||
|
||||
PUBLIC_KEY_FIELDS = REQUIRED_JSON_FIELDS
|
||||
PRIVATE_KEY_FIELDS = ["crv", "d", "x", "y"]
|
||||
|
||||
PUBLIC_KEY_CLS = EllipticCurvePublicKey
|
||||
PRIVATE_KEY_CLS = EllipticCurvePrivateKeyWithSerialization
|
||||
SSH_PUBLIC_PREFIX = b"ecdsa-sha2-"
|
||||
|
||||
def exchange_shared_key(self, pubkey):
|
||||
# # used in ECDHESAlgorithm
|
||||
private_key = self.get_private_key()
|
||||
if private_key:
|
||||
return private_key.exchange(ec.ECDH(), pubkey)
|
||||
raise ValueError("Invalid key for exchanging shared key")
|
||||
|
||||
@property
|
||||
def curve_key_size(self):
|
||||
raw_key = self.get_private_key()
|
||||
if not raw_key:
|
||||
raw_key = self.public_key
|
||||
return raw_key.curve.key_size
|
||||
|
||||
def load_private_key(self):
|
||||
curve = self.DSS_CURVES[self._dict_data["crv"]]()
|
||||
public_numbers = EllipticCurvePublicNumbers(
|
||||
base64_to_int(self._dict_data["x"]),
|
||||
base64_to_int(self._dict_data["y"]),
|
||||
curve,
|
||||
)
|
||||
private_numbers = EllipticCurvePrivateNumbers(
|
||||
base64_to_int(self.tokens["d"]), public_numbers
|
||||
)
|
||||
return private_numbers.private_key(default_backend())
|
||||
|
||||
def load_public_key(self):
|
||||
curve = self.DSS_CURVES[self._dict_data["crv"]]()
|
||||
public_numbers = EllipticCurvePublicNumbers(
|
||||
base64_to_int(self._dict_data["x"]),
|
||||
base64_to_int(self._dict_data["y"]),
|
||||
curve,
|
||||
)
|
||||
return public_numbers.public_key(default_backend())
|
||||
|
||||
def dumps_private_key(self):
|
||||
numbers = self.private_key.private_numbers()
|
||||
return {
|
||||
"crv": self.CURVES_DSS[self.private_key.curve.name],
|
||||
"x": int_to_base64(numbers.public_numbers.x),
|
||||
"y": int_to_base64(numbers.public_numbers.y),
|
||||
"d": int_to_base64(numbers.private_value),
|
||||
}
|
||||
|
||||
def dumps_public_key(self):
|
||||
numbers = self.public_key.public_numbers()
|
||||
return {
|
||||
"crv": self.CURVES_DSS[numbers.curve.name],
|
||||
"x": int_to_base64(numbers.x),
|
||||
"y": int_to_base64(numbers.y),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, crv="P-256", options=None, is_private=False) -> "ECKey":
|
||||
if crv not in cls.DSS_CURVES:
|
||||
raise ValueError(f'Invalid crv value: "{crv}"')
|
||||
raw_key = ec.generate_private_key(
|
||||
curve=cls.DSS_CURVES[crv](),
|
||||
backend=default_backend(),
|
||||
)
|
||||
if not is_private:
|
||||
raw_key = raw_key.public_key()
|
||||
return cls.import_key(raw_key, options=options)
|
||||
@@ -0,0 +1,350 @@
|
||||
import secrets
|
||||
import struct
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
|
||||
from cryptography.hazmat.primitives.keywrap import aes_key_unwrap
|
||||
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_native
|
||||
from authlib.common.encoding import urlsafe_b64decode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
from authlib.jose.rfc7516 import JWEAlgorithm
|
||||
|
||||
from .ec_key import ECKey
|
||||
from .oct_key import OctKey
|
||||
from .rsa_key import RSAKey
|
||||
|
||||
|
||||
class DirectAlgorithm(JWEAlgorithm):
|
||||
name = "dir"
|
||||
description = "Direct use of a shared symmetric key"
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return OctKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
return {}
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
cek = key.get_op_key("encrypt")
|
||||
if len(cek) * 8 != enc_alg.CEK_SIZE:
|
||||
raise ValueError('Invalid "cek" length')
|
||||
return {"ek": b"", "cek": cek}
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
cek = key.get_op_key("decrypt")
|
||||
if len(cek) * 8 != enc_alg.CEK_SIZE:
|
||||
cek = secrets.token_bytes(enc_alg.CEK_SIZE // 8)
|
||||
return cek
|
||||
|
||||
|
||||
class RSAAlgorithm(JWEAlgorithm):
|
||||
#: A key of size 2048 bits or larger MUST be used with these algorithms
|
||||
#: RSA1_5, RSA-OAEP, RSA-OAEP-256
|
||||
key_size = 2048
|
||||
|
||||
def __init__(self, name, description, pad_fn):
|
||||
self.name = name
|
||||
self.deprecated = name == "RSA1_5"
|
||||
self.description = description
|
||||
self.padding = pad_fn
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return RSAKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
cek = enc_alg.generate_cek()
|
||||
return {"cek": cek}
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
if preset and "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = enc_alg.generate_cek()
|
||||
|
||||
op_key = key.get_op_key("wrapKey")
|
||||
if op_key.key_size < self.key_size:
|
||||
raise ValueError("A key of size 2048 bits or larger MUST be used")
|
||||
ek = op_key.encrypt(cek, self.padding)
|
||||
return {"ek": ek, "cek": cek}
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
op_key = key.get_op_key("unwrapKey")
|
||||
cek = op_key.decrypt(ek, self.padding)
|
||||
if len(cek) * 8 != enc_alg.CEK_SIZE:
|
||||
cek = secrets.token_bytes(enc_alg.CEK_SIZE // 8)
|
||||
return cek
|
||||
|
||||
|
||||
class AESAlgorithm(JWEAlgorithm):
|
||||
def __init__(self, key_size):
|
||||
self.name = f"A{key_size}KW"
|
||||
self.description = f"AES Key Wrap using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return OctKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
cek = enc_alg.generate_cek()
|
||||
return {"cek": cek}
|
||||
|
||||
def _check_key(self, key):
|
||||
if len(key) * 8 != self.key_size:
|
||||
raise ValueError(f"A key of size {self.key_size} bits is required.")
|
||||
|
||||
def wrap_cek(self, cek, key):
|
||||
op_key = key.get_op_key("wrapKey")
|
||||
self._check_key(op_key)
|
||||
ek = aes_key_wrap(op_key, cek, default_backend())
|
||||
return {"ek": ek, "cek": cek}
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
if preset and "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = enc_alg.generate_cek()
|
||||
return self.wrap_cek(cek, key)
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
op_key = key.get_op_key("unwrapKey")
|
||||
self._check_key(op_key)
|
||||
cek = aes_key_unwrap(op_key, ek, default_backend())
|
||||
if len(cek) * 8 != enc_alg.CEK_SIZE:
|
||||
cek = secrets.token_bytes(enc_alg.CEK_SIZE // 8)
|
||||
return cek
|
||||
|
||||
|
||||
class AESGCMAlgorithm(JWEAlgorithm):
|
||||
EXTRA_HEADERS = frozenset(["iv", "tag"])
|
||||
|
||||
def __init__(self, key_size):
|
||||
self.name = f"A{key_size}GCMKW"
|
||||
self.description = f"Key wrapping with AES GCM using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return OctKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
cek = enc_alg.generate_cek()
|
||||
return {"cek": cek}
|
||||
|
||||
def _check_key(self, key):
|
||||
if len(key) * 8 != self.key_size:
|
||||
raise ValueError(f"A key of size {self.key_size} bits is required.")
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
if preset and "cek" in preset:
|
||||
cek = preset["cek"]
|
||||
else:
|
||||
cek = enc_alg.generate_cek()
|
||||
|
||||
op_key = key.get_op_key("wrapKey")
|
||||
self._check_key(op_key)
|
||||
|
||||
#: https://tools.ietf.org/html/rfc7518#section-4.7.1.1
|
||||
#: The "iv" (initialization vector) Header Parameter value is the
|
||||
#: base64url-encoded representation of the 96-bit IV value
|
||||
iv_size = 96
|
||||
iv = secrets.token_bytes(iv_size // 8)
|
||||
|
||||
cipher = Cipher(AES(op_key), GCM(iv), backend=default_backend())
|
||||
enc = cipher.encryptor()
|
||||
ek = enc.update(cek) + enc.finalize()
|
||||
|
||||
h = {
|
||||
"iv": to_native(urlsafe_b64encode(iv)),
|
||||
"tag": to_native(urlsafe_b64encode(enc.tag)),
|
||||
}
|
||||
return {"ek": ek, "cek": cek, "header": h}
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
op_key = key.get_op_key("unwrapKey")
|
||||
self._check_key(op_key)
|
||||
|
||||
iv = headers.get("iv")
|
||||
if not iv:
|
||||
raise ValueError('Missing "iv" in headers')
|
||||
|
||||
tag = headers.get("tag")
|
||||
if not tag:
|
||||
raise ValueError('Missing "tag" in headers')
|
||||
|
||||
iv = urlsafe_b64decode(to_bytes(iv))
|
||||
tag = urlsafe_b64decode(to_bytes(tag))
|
||||
|
||||
cipher = Cipher(AES(op_key), GCM(iv, tag), backend=default_backend())
|
||||
d = cipher.decryptor()
|
||||
cek = d.update(ek) + d.finalize()
|
||||
if len(cek) * 8 != enc_alg.CEK_SIZE:
|
||||
cek = secrets.token_bytes(enc_alg.CEK_SIZE // 8)
|
||||
return cek
|
||||
|
||||
|
||||
class ECDHESAlgorithm(JWEAlgorithm):
|
||||
EXTRA_HEADERS = ["epk", "apu", "apv"]
|
||||
ALLOWED_KEY_CLS = ECKey
|
||||
|
||||
# https://tools.ietf.org/html/rfc7518#section-4.6
|
||||
def __init__(self, key_size=None):
|
||||
if key_size is None:
|
||||
self.name = "ECDH-ES"
|
||||
self.description = "ECDH-ES in the Direct Key Agreement mode"
|
||||
else:
|
||||
self.name = f"ECDH-ES+A{key_size}KW"
|
||||
self.description = (
|
||||
f"ECDH-ES using Concat KDF and CEK wrapped with A{key_size}KW"
|
||||
)
|
||||
self.key_size = key_size
|
||||
self.aeskw = AESAlgorithm(key_size)
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
if isinstance(raw_data, self.ALLOWED_KEY_CLS):
|
||||
return raw_data
|
||||
return ECKey.import_key(raw_data)
|
||||
|
||||
def generate_preset(self, enc_alg, key):
|
||||
epk = self._generate_ephemeral_key(key)
|
||||
h = self._prepare_headers(epk)
|
||||
preset = {"epk": epk, "header": h}
|
||||
if self.key_size is not None:
|
||||
cek = enc_alg.generate_cek()
|
||||
preset["cek"] = cek
|
||||
return preset
|
||||
|
||||
def compute_fixed_info(self, headers, bit_size):
|
||||
# AlgorithmID
|
||||
if self.key_size is None:
|
||||
alg_id = u32be_len_input(headers["enc"])
|
||||
else:
|
||||
alg_id = u32be_len_input(headers["alg"])
|
||||
|
||||
# PartyUInfo
|
||||
apu_info = u32be_len_input(headers.get("apu"), True)
|
||||
|
||||
# PartyVInfo
|
||||
apv_info = u32be_len_input(headers.get("apv"), True)
|
||||
|
||||
# SuppPubInfo
|
||||
pub_info = struct.pack(">I", bit_size)
|
||||
|
||||
return alg_id + apu_info + apv_info + pub_info
|
||||
|
||||
def compute_derived_key(self, shared_key, fixed_info, bit_size):
|
||||
ckdf = ConcatKDFHash(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=bit_size // 8,
|
||||
otherinfo=fixed_info,
|
||||
backend=default_backend(),
|
||||
)
|
||||
return ckdf.derive(shared_key)
|
||||
|
||||
def deliver(self, key, pubkey, headers, bit_size):
|
||||
shared_key = key.exchange_shared_key(pubkey)
|
||||
fixed_info = self.compute_fixed_info(headers, bit_size)
|
||||
return self.compute_derived_key(shared_key, fixed_info, bit_size)
|
||||
|
||||
def _generate_ephemeral_key(self, key):
|
||||
return key.generate_key(key["crv"], is_private=True)
|
||||
|
||||
def _prepare_headers(self, epk):
|
||||
# REQUIRED_JSON_FIELDS contains only public fields
|
||||
pub_epk = {k: epk[k] for k in epk.REQUIRED_JSON_FIELDS}
|
||||
pub_epk["kty"] = epk.kty
|
||||
return {"epk": pub_epk}
|
||||
|
||||
def wrap(self, enc_alg, headers, key, preset=None):
|
||||
if self.key_size is None:
|
||||
bit_size = enc_alg.CEK_SIZE
|
||||
else:
|
||||
bit_size = self.key_size
|
||||
|
||||
if preset and "epk" in preset:
|
||||
epk = preset["epk"]
|
||||
h = {}
|
||||
else:
|
||||
epk = self._generate_ephemeral_key(key)
|
||||
h = self._prepare_headers(epk)
|
||||
|
||||
public_key = key.get_op_key("wrapKey")
|
||||
dk = self.deliver(epk, public_key, headers, bit_size)
|
||||
|
||||
if self.key_size is None:
|
||||
return {"ek": b"", "cek": dk, "header": h}
|
||||
|
||||
if preset and "cek" in preset:
|
||||
preset_for_kw = {"cek": preset["cek"]}
|
||||
else:
|
||||
preset_for_kw = None
|
||||
|
||||
kek = self.aeskw.prepare_key(dk)
|
||||
rv = self.aeskw.wrap(enc_alg, headers, kek, preset_for_kw)
|
||||
rv["header"] = h
|
||||
return rv
|
||||
|
||||
def unwrap(self, enc_alg, ek, headers, key):
|
||||
if "epk" not in headers:
|
||||
raise ValueError('Missing "epk" in headers')
|
||||
|
||||
if self.key_size is None:
|
||||
bit_size = enc_alg.CEK_SIZE
|
||||
else:
|
||||
bit_size = self.key_size
|
||||
|
||||
epk = key.import_key(headers["epk"])
|
||||
public_key = epk.get_op_key("wrapKey")
|
||||
dk = self.deliver(key, public_key, headers, bit_size)
|
||||
|
||||
if self.key_size is None:
|
||||
return dk
|
||||
|
||||
kek = self.aeskw.prepare_key(dk)
|
||||
return self.aeskw.unwrap(enc_alg, ek, headers, kek)
|
||||
|
||||
|
||||
def u32be_len_input(s, base64=False):
|
||||
if not s:
|
||||
return b"\x00\x00\x00\x00"
|
||||
if base64:
|
||||
s = urlsafe_b64decode(to_bytes(s))
|
||||
else:
|
||||
s = to_bytes(s)
|
||||
return struct.pack(">I", len(s)) + s
|
||||
|
||||
|
||||
JWE_ALG_ALGORITHMS = [
|
||||
DirectAlgorithm(), # dir
|
||||
RSAAlgorithm("RSA1_5", "RSAES-PKCS1-v1_5", padding.PKCS1v15()),
|
||||
RSAAlgorithm(
|
||||
"RSA-OAEP",
|
||||
"RSAES OAEP using default parameters",
|
||||
padding.OAEP(padding.MGF1(hashes.SHA1()), hashes.SHA1(), None),
|
||||
),
|
||||
RSAAlgorithm(
|
||||
"RSA-OAEP-256",
|
||||
"RSAES OAEP using SHA-256 and MGF1 with SHA-256",
|
||||
padding.OAEP(padding.MGF1(hashes.SHA256()), hashes.SHA256(), None),
|
||||
),
|
||||
AESAlgorithm(128), # A128KW
|
||||
AESAlgorithm(192), # A192KW
|
||||
AESAlgorithm(256), # A256KW
|
||||
AESGCMAlgorithm(128), # A128GCMKW
|
||||
AESGCMAlgorithm(192), # A192GCMKW
|
||||
AESGCMAlgorithm(256), # A256GCMKW
|
||||
ECDHESAlgorithm(None), # ECDH-ES
|
||||
ECDHESAlgorithm(128), # ECDH-ES+A128KW
|
||||
ECDHESAlgorithm(192), # ECDH-ES+A192KW
|
||||
ECDHESAlgorithm(256), # ECDH-ES+A256KW
|
||||
]
|
||||
|
||||
# 'PBES2-HS256+A128KW': '',
|
||||
# 'PBES2-HS384+A192KW': '',
|
||||
# 'PBES2-HS512+A256KW': '',
|
||||
@@ -0,0 +1,147 @@
|
||||
"""authlib.jose.rfc7518.
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Cryptographic Algorithms for Cryptographic Algorithms for Content
|
||||
Encryption per `Section 5`_.
|
||||
|
||||
.. _`Section 5`: https://tools.ietf.org/html/rfc7518#section-5
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from cryptography.exceptions import InvalidTag
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
||||
from cryptography.hazmat.primitives.ciphers.modes import CBC
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
from cryptography.hazmat.primitives.padding import PKCS7
|
||||
|
||||
from ..rfc7516 import JWEEncAlgorithm
|
||||
from .util import encode_int
|
||||
|
||||
|
||||
class CBCHS2EncAlgorithm(JWEEncAlgorithm):
|
||||
# The IV used is a 128-bit value generated randomly or
|
||||
# pseudo-randomly for use in the cipher.
|
||||
IV_SIZE = 128
|
||||
|
||||
def __init__(self, key_size, hash_type):
|
||||
self.name = f"A{key_size}CBC-HS{hash_type}"
|
||||
tpl = "AES_{}_CBC_HMAC_SHA_{} authenticated encryption algorithm"
|
||||
self.description = tpl.format(key_size, hash_type)
|
||||
|
||||
# bit length
|
||||
self.key_size = key_size
|
||||
# byte length
|
||||
self.key_len = key_size // 8
|
||||
|
||||
self.CEK_SIZE = key_size * 2
|
||||
self.hash_alg = getattr(hashlib, f"sha{hash_type}")
|
||||
|
||||
def _hmac(self, ciphertext, aad, iv, key):
|
||||
al = encode_int(len(aad) * 8, 64)
|
||||
msg = aad + iv + ciphertext + al
|
||||
d = hmac.new(key, msg, self.hash_alg).digest()
|
||||
return d[: self.key_len]
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Key Encryption with AES_CBC_HMAC_SHA2.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, iv, tag)
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
hkey = key[: self.key_len]
|
||||
ekey = key[self.key_len :]
|
||||
|
||||
pad = PKCS7(AES.block_size).padder()
|
||||
padded_data = pad.update(msg) + pad.finalize()
|
||||
|
||||
cipher = Cipher(AES(ekey), CBC(iv), backend=default_backend())
|
||||
enc = cipher.encryptor()
|
||||
ciphertext = enc.update(padded_data) + enc.finalize()
|
||||
tag = self._hmac(ciphertext, aad, iv, hkey)
|
||||
return ciphertext, tag
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Key Decryption with AES AES_CBC_HMAC_SHA2.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
hkey = key[: self.key_len]
|
||||
dkey = key[self.key_len :]
|
||||
|
||||
_tag = self._hmac(ciphertext, aad, iv, hkey)
|
||||
if not hmac.compare_digest(_tag, tag):
|
||||
raise InvalidTag()
|
||||
|
||||
cipher = Cipher(AES(dkey), CBC(iv), backend=default_backend())
|
||||
d = cipher.decryptor()
|
||||
data = d.update(ciphertext) + d.finalize()
|
||||
unpad = PKCS7(AES.block_size).unpadder()
|
||||
return unpad.update(data) + unpad.finalize()
|
||||
|
||||
|
||||
class GCMEncAlgorithm(JWEEncAlgorithm):
|
||||
# Use of an IV of size 96 bits is REQUIRED with this algorithm.
|
||||
# https://tools.ietf.org/html/rfc7518#section-5.3
|
||||
IV_SIZE = 96
|
||||
|
||||
def __init__(self, key_size):
|
||||
self.name = f"A{key_size}GCM"
|
||||
self.description = f"AES GCM using {key_size}-bit key"
|
||||
self.key_size = key_size
|
||||
self.CEK_SIZE = key_size
|
||||
|
||||
def encrypt(self, msg, aad, iv, key):
|
||||
"""Key Encryption with AES GCM.
|
||||
|
||||
:param msg: text to be encrypt in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: (ciphertext, iv, tag)
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
cipher = Cipher(AES(key), GCM(iv), backend=default_backend())
|
||||
enc = cipher.encryptor()
|
||||
enc.authenticate_additional_data(aad)
|
||||
ciphertext = enc.update(msg) + enc.finalize()
|
||||
return ciphertext, enc.tag
|
||||
|
||||
def decrypt(self, ciphertext, aad, iv, tag, key):
|
||||
"""Key Decryption with AES GCM.
|
||||
|
||||
:param ciphertext: ciphertext in bytes
|
||||
:param aad: additional authenticated data in bytes
|
||||
:param iv: initialization vector in bytes
|
||||
:param tag: authentication tag in bytes
|
||||
:param key: encrypted key in bytes
|
||||
:return: message
|
||||
"""
|
||||
self.check_iv(iv)
|
||||
cipher = Cipher(AES(key), GCM(iv, tag), backend=default_backend())
|
||||
d = cipher.decryptor()
|
||||
d.authenticate_additional_data(aad)
|
||||
return d.update(ciphertext) + d.finalize()
|
||||
|
||||
|
||||
JWE_ENC_ALGORITHMS = [
|
||||
CBCHS2EncAlgorithm(128, 256), # A128CBC-HS256
|
||||
CBCHS2EncAlgorithm(192, 384), # A192CBC-HS384
|
||||
CBCHS2EncAlgorithm(256, 512), # A256CBC-HS512
|
||||
GCMEncAlgorithm(128), # A128GCM
|
||||
GCMEncAlgorithm(192), # A192GCM
|
||||
GCMEncAlgorithm(256), # A256GCM
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
import zlib
|
||||
|
||||
from ..rfc7516 import JsonWebEncryption
|
||||
from ..rfc7516 import JWEZipAlgorithm
|
||||
|
||||
GZIP_HEAD = bytes([120, 156])
|
||||
MAX_SIZE = 250 * 1024
|
||||
|
||||
|
||||
class DeflateZipAlgorithm(JWEZipAlgorithm):
|
||||
name = "DEF"
|
||||
description = "DEFLATE"
|
||||
|
||||
def compress(self, s: bytes) -> bytes:
|
||||
"""Compress bytes data with DEFLATE algorithm."""
|
||||
data = zlib.compress(s)
|
||||
# https://datatracker.ietf.org/doc/html/rfc1951
|
||||
# since DEF is always gzip, we can drop gzip headers and tail
|
||||
return data[2:-4]
|
||||
|
||||
def decompress(self, s: bytes) -> bytes:
|
||||
"""Decompress DEFLATE bytes data."""
|
||||
if s.startswith(GZIP_HEAD):
|
||||
decompressor = zlib.decompressobj()
|
||||
else:
|
||||
decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
value = decompressor.decompress(s, MAX_SIZE)
|
||||
if decompressor.unconsumed_tail:
|
||||
raise ValueError(f"Decompressed string exceeds {MAX_SIZE} bytes")
|
||||
return value
|
||||
|
||||
|
||||
def register_jwe_rfc7518():
|
||||
JsonWebEncryption.register_algorithm(DeflateZipAlgorithm())
|
||||
@@ -0,0 +1,222 @@
|
||||
"""authlib.jose.rfc7518.
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
"alg" (Algorithm) Header Parameter Values for JWS per `Section 3`_.
|
||||
|
||||
.. _`Section 3`: https://tools.ietf.org/html/rfc7518#section-3
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
||||
|
||||
from ..rfc7515 import JWSAlgorithm
|
||||
from .ec_key import ECKey
|
||||
from .oct_key import OctKey
|
||||
from .rsa_key import RSAKey
|
||||
from .util import decode_int
|
||||
from .util import encode_int
|
||||
|
||||
|
||||
class NoneAlgorithm(JWSAlgorithm):
|
||||
name = "none"
|
||||
description = "No digital signature or MAC performed"
|
||||
deprecated = True
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return None
|
||||
|
||||
def sign(self, msg, key):
|
||||
return b""
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
return sig == b""
|
||||
|
||||
|
||||
class HMACAlgorithm(JWSAlgorithm):
|
||||
"""HMAC using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- HS256: HMAC using SHA-256
|
||||
- HS384: HMAC using SHA-384
|
||||
- HS512: HMAC using SHA-512
|
||||
"""
|
||||
|
||||
SHA256 = hashlib.sha256
|
||||
SHA384 = hashlib.sha384
|
||||
SHA512 = hashlib.sha512
|
||||
|
||||
def __init__(self, sha_type):
|
||||
self.name = f"HS{sha_type}"
|
||||
self.description = f"HMAC using SHA-{sha_type}"
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return OctKey.import_key(raw_data)
|
||||
|
||||
def sign(self, msg, key):
|
||||
# it is faster than the one in cryptography
|
||||
op_key = key.get_op_key("sign")
|
||||
return hmac.new(op_key, msg, self.hash_alg).digest()
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
op_key = key.get_op_key("verify")
|
||||
v_sig = hmac.new(op_key, msg, self.hash_alg).digest()
|
||||
return hmac.compare_digest(sig, v_sig)
|
||||
|
||||
|
||||
class RSAAlgorithm(JWSAlgorithm):
|
||||
"""RSA using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- RS256: RSASSA-PKCS1-v1_5 using SHA-256
|
||||
- RS384: RSASSA-PKCS1-v1_5 using SHA-384
|
||||
- RS512: RSASSA-PKCS1-v1_5 using SHA-512
|
||||
"""
|
||||
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, sha_type):
|
||||
self.name = f"RS{sha_type}"
|
||||
self.description = f"RSASSA-PKCS1-v1_5 using SHA-{sha_type}"
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
self.padding = padding.PKCS1v15()
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return RSAKey.import_key(raw_data)
|
||||
|
||||
def sign(self, msg, key):
|
||||
op_key = key.get_op_key("sign")
|
||||
return op_key.sign(msg, self.padding, self.hash_alg())
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
op_key = key.get_op_key("verify")
|
||||
try:
|
||||
op_key.verify(sig, msg, self.padding, self.hash_alg())
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
class ECAlgorithm(JWSAlgorithm):
|
||||
"""ECDSA using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- ES256: ECDSA using P-256 and SHA-256
|
||||
- ES384: ECDSA using P-384 and SHA-384
|
||||
- ES512: ECDSA using P-521 and SHA-512
|
||||
"""
|
||||
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, name, curve, sha_type):
|
||||
self.name = name
|
||||
self.curve = curve
|
||||
self.description = f"ECDSA using {self.curve} and SHA-{sha_type}"
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
key = ECKey.import_key(raw_data)
|
||||
if key["crv"] != self.curve:
|
||||
raise ValueError(
|
||||
f'Key for "{self.name}" not supported, only "{self.curve}" allowed'
|
||||
)
|
||||
return key
|
||||
|
||||
def sign(self, msg, key):
|
||||
op_key = key.get_op_key("sign")
|
||||
der_sig = op_key.sign(msg, ECDSA(self.hash_alg()))
|
||||
r, s = decode_dss_signature(der_sig)
|
||||
size = key.curve_key_size
|
||||
return encode_int(r, size) + encode_int(s, size)
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
key_size = key.curve_key_size
|
||||
length = (key_size + 7) // 8
|
||||
|
||||
if len(sig) != 2 * length:
|
||||
return False
|
||||
|
||||
r = decode_int(sig[:length])
|
||||
s = decode_int(sig[length:])
|
||||
der_sig = encode_dss_signature(r, s)
|
||||
|
||||
try:
|
||||
op_key = key.get_op_key("verify")
|
||||
op_key.verify(der_sig, msg, ECDSA(self.hash_alg()))
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
class RSAPSSAlgorithm(JWSAlgorithm):
|
||||
"""RSASSA-PSS using SHA algorithms for JWS. Available algorithms:
|
||||
|
||||
- PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
|
||||
- PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
|
||||
- PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
|
||||
"""
|
||||
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, sha_type):
|
||||
self.name = f"PS{sha_type}"
|
||||
tpl = "RSASSA-PSS using SHA-{} and MGF1 with SHA-{}"
|
||||
self.description = tpl.format(sha_type, sha_type)
|
||||
self.hash_alg = getattr(self, f"SHA{sha_type}")
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return RSAKey.import_key(raw_data)
|
||||
|
||||
def sign(self, msg, key):
|
||||
op_key = key.get_op_key("sign")
|
||||
return op_key.sign(
|
||||
msg,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg.digest_size
|
||||
),
|
||||
self.hash_alg(),
|
||||
)
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
op_key = key.get_op_key("verify")
|
||||
try:
|
||||
op_key.verify(
|
||||
sig,
|
||||
msg,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(self.hash_alg()),
|
||||
salt_length=self.hash_alg.digest_size,
|
||||
),
|
||||
self.hash_alg(),
|
||||
)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
JWS_ALGORITHMS = [
|
||||
NoneAlgorithm(), # none
|
||||
HMACAlgorithm(256), # HS256
|
||||
HMACAlgorithm(384), # HS384
|
||||
HMACAlgorithm(512), # HS512
|
||||
RSAAlgorithm(256), # RS256
|
||||
RSAAlgorithm(384), # RS384
|
||||
RSAAlgorithm(512), # RS512
|
||||
ECAlgorithm("ES256", "P-256", 256),
|
||||
ECAlgorithm("ES384", "P-384", 384),
|
||||
ECAlgorithm("ES512", "P-521", 512),
|
||||
ECAlgorithm("ES256K", "secp256k1", 256), # defined in RFC8812
|
||||
RSAPSSAlgorithm(256), # PS256
|
||||
RSAPSSAlgorithm(384), # PS384
|
||||
RSAPSSAlgorithm(512), # PS512
|
||||
]
|
||||
@@ -0,0 +1,96 @@
|
||||
import secrets
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64decode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
|
||||
from ..rfc7517 import Key
|
||||
|
||||
POSSIBLE_UNSAFE_KEYS = (
|
||||
b"-----BEGIN ",
|
||||
b"---- BEGIN ",
|
||||
b"ssh-rsa ",
|
||||
b"ssh-dss ",
|
||||
b"ssh-ed25519 ",
|
||||
b"ecdsa-sha2-",
|
||||
)
|
||||
|
||||
|
||||
class OctKey(Key):
|
||||
"""Key class of the ``oct`` key type."""
|
||||
|
||||
kty = "oct"
|
||||
REQUIRED_JSON_FIELDS = ["k"]
|
||||
|
||||
def __init__(self, raw_key=None, options=None):
|
||||
super().__init__(options)
|
||||
self.raw_key = raw_key
|
||||
|
||||
@property
|
||||
def public_only(self):
|
||||
return False
|
||||
|
||||
def get_op_key(self, operation):
|
||||
"""Get the raw key for the given key_op. This method will also
|
||||
check if the given key_op is supported by this key.
|
||||
|
||||
:param operation: key operation value, such as "sign", "encrypt".
|
||||
:return: raw key
|
||||
"""
|
||||
self.check_key_op(operation)
|
||||
if not self.raw_key:
|
||||
self.load_raw_key()
|
||||
return self.raw_key
|
||||
|
||||
def load_raw_key(self):
|
||||
self.raw_key = urlsafe_b64decode(to_bytes(self.tokens["k"]))
|
||||
|
||||
def load_dict_key(self):
|
||||
k = to_unicode(urlsafe_b64encode(self.raw_key))
|
||||
self._dict_data = {"kty": self.kty, "k": k}
|
||||
|
||||
def as_dict(self, is_private=False, **params):
|
||||
tokens = self.tokens
|
||||
if "kid" not in tokens:
|
||||
tokens["kid"] = self.thumbprint()
|
||||
|
||||
tokens.update(params)
|
||||
return tokens
|
||||
|
||||
@classmethod
|
||||
def validate_raw_key(cls, key):
|
||||
return isinstance(key, bytes)
|
||||
|
||||
@classmethod
|
||||
def import_key(cls, raw, options=None):
|
||||
"""Import a key from bytes, string, or dict data."""
|
||||
if isinstance(raw, cls):
|
||||
if options is not None:
|
||||
raw.options.update(options)
|
||||
return raw
|
||||
|
||||
if isinstance(raw, dict):
|
||||
cls.check_required_fields(raw)
|
||||
key = cls(options=options)
|
||||
key._dict_data = raw
|
||||
else:
|
||||
raw_key = to_bytes(raw)
|
||||
|
||||
# security check
|
||||
if raw_key.startswith(POSSIBLE_UNSAFE_KEYS):
|
||||
raise ValueError("This key may not be safe to import")
|
||||
|
||||
key = cls(raw_key=raw_key, options=options)
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, key_size=256, options=None, is_private=True):
|
||||
"""Generate a ``OctKey`` with the given bit size."""
|
||||
if not is_private:
|
||||
raise ValueError("oct key can not be generated as public")
|
||||
|
||||
if key_size % 8 != 0:
|
||||
raise ValueError("Invalid bit size for oct key")
|
||||
|
||||
return cls.import_key(secrets.token_bytes(int(key_size / 8)), options)
|
||||
@@ -0,0 +1,127 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKeyWithSerialization
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmq1
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_iqmp
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_recover_prime_factors
|
||||
|
||||
from authlib.common.encoding import base64_to_int
|
||||
from authlib.common.encoding import int_to_base64
|
||||
|
||||
from ..rfc7517 import AsymmetricKey
|
||||
|
||||
|
||||
class RSAKey(AsymmetricKey):
|
||||
"""Key class of the ``RSA`` key type."""
|
||||
|
||||
kty = "RSA"
|
||||
PUBLIC_KEY_CLS = RSAPublicKey
|
||||
PRIVATE_KEY_CLS = RSAPrivateKeyWithSerialization
|
||||
|
||||
PUBLIC_KEY_FIELDS = ["e", "n"]
|
||||
PRIVATE_KEY_FIELDS = ["d", "dp", "dq", "e", "n", "p", "q", "qi"]
|
||||
REQUIRED_JSON_FIELDS = ["e", "n"]
|
||||
SSH_PUBLIC_PREFIX = b"ssh-rsa"
|
||||
|
||||
def dumps_private_key(self):
|
||||
numbers = self.private_key.private_numbers()
|
||||
return {
|
||||
"n": int_to_base64(numbers.public_numbers.n),
|
||||
"e": int_to_base64(numbers.public_numbers.e),
|
||||
"d": int_to_base64(numbers.d),
|
||||
"p": int_to_base64(numbers.p),
|
||||
"q": int_to_base64(numbers.q),
|
||||
"dp": int_to_base64(numbers.dmp1),
|
||||
"dq": int_to_base64(numbers.dmq1),
|
||||
"qi": int_to_base64(numbers.iqmp),
|
||||
}
|
||||
|
||||
def dumps_public_key(self):
|
||||
numbers = self.public_key.public_numbers()
|
||||
return {"n": int_to_base64(numbers.n), "e": int_to_base64(numbers.e)}
|
||||
|
||||
def load_private_key(self):
|
||||
obj = self._dict_data
|
||||
|
||||
if "oth" in obj: # pragma: no cover
|
||||
# https://tools.ietf.org/html/rfc7518#section-6.3.2.7
|
||||
raise ValueError('"oth" is not supported yet')
|
||||
|
||||
public_numbers = RSAPublicNumbers(
|
||||
base64_to_int(obj["e"]), base64_to_int(obj["n"])
|
||||
)
|
||||
|
||||
if has_all_prime_factors(obj):
|
||||
numbers = RSAPrivateNumbers(
|
||||
d=base64_to_int(obj["d"]),
|
||||
p=base64_to_int(obj["p"]),
|
||||
q=base64_to_int(obj["q"]),
|
||||
dmp1=base64_to_int(obj["dp"]),
|
||||
dmq1=base64_to_int(obj["dq"]),
|
||||
iqmp=base64_to_int(obj["qi"]),
|
||||
public_numbers=public_numbers,
|
||||
)
|
||||
else:
|
||||
d = base64_to_int(obj["d"])
|
||||
p, q = rsa_recover_prime_factors(public_numbers.n, d, public_numbers.e)
|
||||
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=public_numbers,
|
||||
)
|
||||
|
||||
return numbers.private_key(default_backend())
|
||||
|
||||
def load_public_key(self):
|
||||
numbers = RSAPublicNumbers(
|
||||
base64_to_int(self._dict_data["e"]), base64_to_int(self._dict_data["n"])
|
||||
)
|
||||
return numbers.public_key(default_backend())
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, key_size=2048, options=None, is_private=False) -> "RSAKey":
|
||||
if key_size < 512:
|
||||
raise ValueError("key_size must not be less than 512")
|
||||
if key_size % 8 != 0:
|
||||
raise ValueError("Invalid key_size for RSAKey")
|
||||
raw_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=key_size,
|
||||
backend=default_backend(),
|
||||
)
|
||||
if not is_private:
|
||||
raw_key = raw_key.public_key()
|
||||
return cls.import_key(raw_key, options=options)
|
||||
|
||||
@classmethod
|
||||
def import_dict_key(cls, raw, options=None):
|
||||
cls.check_required_fields(raw)
|
||||
key = cls(options=options)
|
||||
key._dict_data = raw
|
||||
if "d" in raw and not has_all_prime_factors(raw):
|
||||
# reload dict key
|
||||
key.load_raw_key()
|
||||
key.load_dict_key()
|
||||
return key
|
||||
|
||||
|
||||
def has_all_prime_factors(obj):
|
||||
props = ["p", "q", "dp", "dq", "qi"]
|
||||
props_found = [prop in obj for prop in props]
|
||||
if all(props_found):
|
||||
return True
|
||||
|
||||
if any(props_found):
|
||||
raise ValueError(
|
||||
"RSA key must include all parameters if any are present besides d"
|
||||
)
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,12 @@
|
||||
import binascii
|
||||
|
||||
|
||||
def encode_int(num, bits):
|
||||
length = ((bits + 7) // 8) * 2
|
||||
padded_hex = f"{num:0{length}x}"
|
||||
big_endian = binascii.a2b_hex(padded_hex.encode("ascii"))
|
||||
return big_endian
|
||||
|
||||
|
||||
def decode_int(b):
|
||||
return int(binascii.b2a_hex(b), 16)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""authlib.jose.rfc7519.
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
JSON Web Token (JWT).
|
||||
|
||||
https://tools.ietf.org/html/rfc7519
|
||||
"""
|
||||
|
||||
from .claims import BaseClaims
|
||||
from .claims import JWTClaims
|
||||
from .jwt import JsonWebToken
|
||||
|
||||
__all__ = ["JsonWebToken", "BaseClaims", "JWTClaims"]
|
||||
@@ -0,0 +1,227 @@
|
||||
import time
|
||||
|
||||
from authlib.jose.errors import ExpiredTokenError
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
from authlib.jose.errors import InvalidTokenError
|
||||
from authlib.jose.errors import MissingClaimError
|
||||
|
||||
|
||||
class BaseClaims(dict):
|
||||
"""Payload claims for JWT, which contains a validate interface.
|
||||
|
||||
:param payload: the payload dict of JWT
|
||||
:param header: the header dict of JWT
|
||||
:param options: validate options
|
||||
:param params: other params
|
||||
|
||||
An example on ``options`` parameter, the format is inspired by
|
||||
`OpenID Connect Claims`_::
|
||||
|
||||
{
|
||||
"iss": {
|
||||
"essential": True,
|
||||
"values": ["https://example.com", "https://example.org"]
|
||||
},
|
||||
"sub": {
|
||||
"essential": True
|
||||
"value": "248289761001"
|
||||
},
|
||||
"jti": {
|
||||
"validate": validate_jti
|
||||
}
|
||||
}
|
||||
|
||||
.. _`OpenID Connect Claims`:
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
|
||||
"""
|
||||
|
||||
REGISTERED_CLAIMS = []
|
||||
|
||||
def __init__(self, payload, header, options=None, params=None):
|
||||
super().__init__(payload)
|
||||
self.header = header
|
||||
self.options = options or {}
|
||||
self.params = params or {}
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return object.__getattribute__(self, key)
|
||||
except AttributeError as error:
|
||||
if key in self.REGISTERED_CLAIMS:
|
||||
return self.get(key)
|
||||
raise error
|
||||
|
||||
def _validate_essential_claims(self):
|
||||
for k in self.options:
|
||||
if self.options[k].get("essential"):
|
||||
if k not in self:
|
||||
raise MissingClaimError(k)
|
||||
elif not self.get(k):
|
||||
raise InvalidClaimError(k)
|
||||
|
||||
def _validate_claim_value(self, claim_name):
|
||||
option = self.options.get(claim_name)
|
||||
if not option:
|
||||
return
|
||||
|
||||
value = self.get(claim_name)
|
||||
option_value = option.get("value")
|
||||
if option_value and value != option_value:
|
||||
raise InvalidClaimError(claim_name)
|
||||
|
||||
option_values = option.get("values")
|
||||
if option_values and value not in option_values:
|
||||
raise InvalidClaimError(claim_name)
|
||||
|
||||
validate = option.get("validate")
|
||||
if validate and not validate(self, value):
|
||||
raise InvalidClaimError(claim_name)
|
||||
|
||||
def get_registered_claims(self): # pragma: no cover
|
||||
rv = {}
|
||||
for k in self.REGISTERED_CLAIMS:
|
||||
if k in self:
|
||||
rv[k] = self[k]
|
||||
return rv
|
||||
|
||||
|
||||
class JWTClaims(BaseClaims):
|
||||
REGISTERED_CLAIMS = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"]
|
||||
|
||||
def validate(self, now=None, leeway=0):
|
||||
"""Validate everything in claims payload."""
|
||||
self._validate_essential_claims()
|
||||
|
||||
if now is None:
|
||||
now = int(time.time())
|
||||
|
||||
self.validate_iss()
|
||||
self.validate_sub()
|
||||
self.validate_aud()
|
||||
self.validate_exp(now, leeway)
|
||||
self.validate_nbf(now, leeway)
|
||||
self.validate_iat(now, leeway)
|
||||
self.validate_jti()
|
||||
|
||||
# Validate custom claims
|
||||
for key in self.options.keys():
|
||||
if key not in self.REGISTERED_CLAIMS:
|
||||
self._validate_claim_value(key)
|
||||
|
||||
def validate_iss(self):
|
||||
"""The "iss" (issuer) claim identifies the principal that issued the
|
||||
JWT. The processing of this claim is generally application specific.
|
||||
The "iss" value is a case-sensitive string containing a StringOrURI
|
||||
value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
self._validate_claim_value("iss")
|
||||
|
||||
def validate_sub(self):
|
||||
"""The "sub" (subject) claim identifies the principal that is the
|
||||
subject of the JWT. The claims in a JWT are normally statements
|
||||
about the subject. The subject value MUST either be scoped to be
|
||||
locally unique in the context of the issuer or be globally unique.
|
||||
The processing of this claim is generally application specific. The
|
||||
"sub" value is a case-sensitive string containing a StringOrURI
|
||||
value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
self._validate_claim_value("sub")
|
||||
|
||||
def validate_aud(self):
|
||||
"""The "aud" (audience) claim identifies the recipients that the JWT is
|
||||
intended for. Each principal intended to process the JWT MUST
|
||||
identify itself with a value in the audience claim. If the principal
|
||||
processing the claim does not identify itself with a value in the
|
||||
"aud" claim when this claim is present, then the JWT MUST be
|
||||
rejected. In the general case, the "aud" value is an array of case-
|
||||
sensitive strings, each containing a StringOrURI value. In the
|
||||
special case when the JWT has one audience, the "aud" value MAY be a
|
||||
single case-sensitive string containing a StringOrURI value. The
|
||||
interpretation of audience values is generally application specific.
|
||||
Use of this claim is OPTIONAL.
|
||||
"""
|
||||
aud_option = self.options.get("aud")
|
||||
aud = self.get("aud")
|
||||
if not aud_option or not aud:
|
||||
return
|
||||
|
||||
aud_values = aud_option.get("values")
|
||||
if not aud_values:
|
||||
aud_value = aud_option.get("value")
|
||||
if aud_value:
|
||||
aud_values = [aud_value]
|
||||
|
||||
if not aud_values:
|
||||
return
|
||||
|
||||
if isinstance(self["aud"], list):
|
||||
aud_list = self["aud"]
|
||||
else:
|
||||
aud_list = [self["aud"]]
|
||||
|
||||
if not any([v in aud_list for v in aud_values]):
|
||||
raise InvalidClaimError("aud")
|
||||
|
||||
def validate_exp(self, now, leeway):
|
||||
"""The "exp" (expiration time) claim identifies the expiration time on
|
||||
or after which the JWT MUST NOT be accepted for processing. The
|
||||
processing of the "exp" claim requires that the current date/time
|
||||
MUST be before the expiration date/time listed in the "exp" claim.
|
||||
Implementers MAY provide for some small leeway, usually no more than
|
||||
a few minutes, to account for clock skew. Its value MUST be a number
|
||||
containing a NumericDate value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if "exp" in self:
|
||||
exp = self["exp"]
|
||||
if not _validate_numeric_time(exp):
|
||||
raise InvalidClaimError("exp")
|
||||
if exp < (now - leeway):
|
||||
raise ExpiredTokenError()
|
||||
|
||||
def validate_nbf(self, now, leeway):
|
||||
"""The "nbf" (not before) claim identifies the time before which the JWT
|
||||
MUST NOT be accepted for processing. The processing of the "nbf"
|
||||
claim requires that the current date/time MUST be after or equal to
|
||||
the not-before date/time listed in the "nbf" claim. Implementers MAY
|
||||
provide for some small leeway, usually no more than a few minutes, to
|
||||
account for clock skew. Its value MUST be a number containing a
|
||||
NumericDate value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if "nbf" in self:
|
||||
nbf = self["nbf"]
|
||||
if not _validate_numeric_time(nbf):
|
||||
raise InvalidClaimError("nbf")
|
||||
if nbf > (now + leeway):
|
||||
raise InvalidTokenError()
|
||||
|
||||
def validate_iat(self, now, leeway):
|
||||
"""The "iat" (issued at) claim identifies the time at which the JWT was
|
||||
issued. This claim can be used to determine the age of the JWT.
|
||||
Implementers MAY provide for some small leeway, usually no more
|
||||
than a few minutes, to account for clock skew. Its value MUST be a
|
||||
number containing a NumericDate value. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
if "iat" in self:
|
||||
iat = self["iat"]
|
||||
if not _validate_numeric_time(iat):
|
||||
raise InvalidClaimError("iat")
|
||||
if iat > (now + leeway):
|
||||
raise InvalidTokenError(
|
||||
description="The token is not valid as it was issued in the future"
|
||||
)
|
||||
|
||||
def validate_jti(self):
|
||||
"""The "jti" (JWT ID) claim provides a unique identifier for the JWT.
|
||||
The identifier value MUST be assigned in a manner that ensures that
|
||||
there is a negligible probability that the same value will be
|
||||
accidentally assigned to a different data object; if the application
|
||||
uses multiple issuers, collisions MUST be prevented among values
|
||||
produced by different issuers as well. The "jti" claim can be used
|
||||
to prevent the JWT from being replayed. The "jti" value is a case-
|
||||
sensitive string. Use of this claim is OPTIONAL.
|
||||
"""
|
||||
self._validate_claim_value("jti")
|
||||
|
||||
|
||||
def _validate_numeric_time(s):
|
||||
return isinstance(s, (int, float))
|
||||
@@ -0,0 +1,191 @@
|
||||
import calendar
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
|
||||
from authlib.common.encoding import json_dumps
|
||||
from authlib.common.encoding import json_loads
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
|
||||
from ..errors import DecodeError
|
||||
from ..errors import InsecureClaimError
|
||||
from ..rfc7515 import JsonWebSignature
|
||||
from ..rfc7516 import JsonWebEncryption
|
||||
from ..rfc7517 import Key
|
||||
from ..rfc7517 import KeySet
|
||||
from .claims import JWTClaims
|
||||
|
||||
|
||||
class JsonWebToken:
|
||||
SENSITIVE_NAMES = ("password", "token", "secret", "secret_key")
|
||||
# Thanks to sentry SensitiveDataFilter
|
||||
SENSITIVE_VALUES = re.compile(
|
||||
r"|".join(
|
||||
[
|
||||
# http://www.richardsramblings.com/regex/credit-card-numbers/
|
||||
r"\b(?:3[47]\d|(?:4\d|5[1-5]|65)\d{2}|6011)\d{12}\b",
|
||||
# various private keys
|
||||
r"-----BEGIN[A-Z ]+PRIVATE KEY-----.+-----END[A-Z ]+PRIVATE KEY-----",
|
||||
# social security numbers (US)
|
||||
r"^\b(?!(000|666|9))\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b",
|
||||
]
|
||||
),
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def __init__(self, algorithms, private_headers=None):
|
||||
self._jws = JsonWebSignature(algorithms, private_headers=private_headers)
|
||||
self._jwe = JsonWebEncryption(algorithms, private_headers=private_headers)
|
||||
|
||||
def check_sensitive_data(self, payload):
|
||||
"""Check if payload contains sensitive information."""
|
||||
for k in payload:
|
||||
# check claims key name
|
||||
if k in self.SENSITIVE_NAMES:
|
||||
raise InsecureClaimError(k)
|
||||
|
||||
# check claims values
|
||||
v = payload[k]
|
||||
if isinstance(v, str) and self.SENSITIVE_VALUES.search(v):
|
||||
raise InsecureClaimError(k)
|
||||
|
||||
def encode(self, header, payload, key, check=True):
|
||||
"""Encode a JWT with the given header, payload and key.
|
||||
|
||||
:param header: A dict of JWS header
|
||||
:param payload: A dict to be encoded
|
||||
:param key: key used to sign the signature
|
||||
:param check: check if sensitive data in payload
|
||||
:return: bytes
|
||||
"""
|
||||
header.setdefault("typ", "JWT")
|
||||
|
||||
for k in ["exp", "iat", "nbf"]:
|
||||
# convert datetime into timestamp
|
||||
claim = payload.get(k)
|
||||
if isinstance(claim, datetime.datetime):
|
||||
payload[k] = calendar.timegm(claim.utctimetuple())
|
||||
|
||||
if check:
|
||||
self.check_sensitive_data(payload)
|
||||
|
||||
key = find_encode_key(key, header)
|
||||
text = to_bytes(json_dumps(payload))
|
||||
if "enc" in header:
|
||||
return self._jwe.serialize_compact(header, text, key)
|
||||
else:
|
||||
return self._jws.serialize_compact(header, text, key)
|
||||
|
||||
def decode(self, s, key, claims_cls=None, claims_options=None, claims_params=None):
|
||||
"""Decode the JWT with the given key. This is similar with
|
||||
:meth:`verify`, except that it will raise BadSignatureError when
|
||||
signature doesn't match.
|
||||
|
||||
:param s: text of JWT
|
||||
:param key: key used to verify the signature
|
||||
:param claims_cls: class to be used for JWT claims
|
||||
:param claims_options: `options` parameters for claims_cls
|
||||
:param claims_params: `params` parameters for claims_cls
|
||||
:return: claims_cls instance
|
||||
:raise: BadSignatureError
|
||||
"""
|
||||
if claims_cls is None:
|
||||
claims_cls = JWTClaims
|
||||
|
||||
if callable(key):
|
||||
load_key = key
|
||||
else:
|
||||
load_key = create_load_key(prepare_raw_key(key))
|
||||
|
||||
s = to_bytes(s)
|
||||
dot_count = s.count(b".")
|
||||
if dot_count == 2:
|
||||
data = self._jws.deserialize_compact(s, load_key, decode_payload)
|
||||
elif dot_count == 4:
|
||||
data = self._jwe.deserialize_compact(s, load_key, decode_payload)
|
||||
else:
|
||||
raise DecodeError("Invalid input segments length")
|
||||
return claims_cls(
|
||||
data["payload"],
|
||||
data["header"],
|
||||
options=claims_options,
|
||||
params=claims_params,
|
||||
)
|
||||
|
||||
|
||||
def decode_payload(bytes_payload):
|
||||
try:
|
||||
payload = json_loads(to_unicode(bytes_payload))
|
||||
except ValueError as exc:
|
||||
raise DecodeError("Invalid payload value") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise DecodeError("Invalid payload type")
|
||||
return payload
|
||||
|
||||
|
||||
def prepare_raw_key(raw):
|
||||
if isinstance(raw, KeySet):
|
||||
return raw
|
||||
|
||||
if isinstance(raw, str) and raw.startswith("{") and raw.endswith("}"):
|
||||
raw = json_loads(raw)
|
||||
elif isinstance(raw, (tuple, list)):
|
||||
raw = {"keys": raw}
|
||||
return raw
|
||||
|
||||
|
||||
def find_encode_key(key, header):
|
||||
if isinstance(key, KeySet):
|
||||
kid = header.get("kid")
|
||||
if kid:
|
||||
return key.find_by_kid(kid)
|
||||
|
||||
rv = random.choice(key.keys)
|
||||
# use side effect to add kid value into header
|
||||
header["kid"] = rv.kid
|
||||
return rv
|
||||
|
||||
if isinstance(key, dict) and "keys" in key:
|
||||
keys = key["keys"]
|
||||
kid = header.get("kid")
|
||||
for k in keys:
|
||||
if k.get("kid") == kid:
|
||||
return k
|
||||
|
||||
if not kid:
|
||||
rv = random.choice(keys)
|
||||
header["kid"] = rv["kid"]
|
||||
return rv
|
||||
raise ValueError("Invalid JSON Web Key Set")
|
||||
|
||||
# append kid into header
|
||||
if isinstance(key, dict) and "kid" in key:
|
||||
header["kid"] = key["kid"]
|
||||
elif isinstance(key, Key) and key.kid:
|
||||
header["kid"] = key.kid
|
||||
return key
|
||||
|
||||
|
||||
def create_load_key(key):
|
||||
def load_key(header, payload):
|
||||
if isinstance(key, KeySet):
|
||||
return key.find_by_kid(header.get("kid"))
|
||||
|
||||
if isinstance(key, dict) and "keys" in key:
|
||||
keys = key["keys"]
|
||||
kid = header.get("kid")
|
||||
|
||||
if kid is not None:
|
||||
# look for the requested key
|
||||
for k in keys:
|
||||
if k.get("kid") == kid:
|
||||
return k
|
||||
else:
|
||||
# use the only key
|
||||
if len(keys) == 1:
|
||||
return keys[0]
|
||||
raise ValueError("Invalid JSON Web Key Set")
|
||||
return key
|
||||
|
||||
return load_key
|
||||
@@ -0,0 +1,4 @@
|
||||
from .jws_eddsa import register_jws_rfc8037
|
||||
from .okp_key import OKPKey
|
||||
|
||||
__all__ = ["register_jws_rfc8037", "OKPKey"]
|
||||
@@ -0,0 +1,28 @@
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
from ..rfc7515 import JWSAlgorithm
|
||||
from .okp_key import OKPKey
|
||||
|
||||
|
||||
class EdDSAAlgorithm(JWSAlgorithm):
|
||||
name = "EdDSA"
|
||||
description = "Edwards-curve Digital Signature Algorithm for JWS"
|
||||
|
||||
def prepare_key(self, raw_data):
|
||||
return OKPKey.import_key(raw_data)
|
||||
|
||||
def sign(self, msg, key):
|
||||
op_key = key.get_op_key("sign")
|
||||
return op_key.sign(msg)
|
||||
|
||||
def verify(self, msg, sig, key):
|
||||
op_key = key.get_op_key("verify")
|
||||
try:
|
||||
op_key.verify(sig, msg)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
def register_jws_rfc8037(cls):
|
||||
cls.register_algorithm(EdDSAAlgorithm())
|
||||
@@ -0,0 +1,99 @@
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.hazmat.primitives.serialization import NoEncryption
|
||||
from cryptography.hazmat.primitives.serialization import PrivateFormat
|
||||
from cryptography.hazmat.primitives.serialization import PublicFormat
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64decode
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
|
||||
from ..rfc7517 import AsymmetricKey
|
||||
|
||||
PUBLIC_KEYS_MAP = {
|
||||
"Ed25519": Ed25519PublicKey,
|
||||
"Ed448": Ed448PublicKey,
|
||||
"X25519": X25519PublicKey,
|
||||
"X448": X448PublicKey,
|
||||
}
|
||||
PRIVATE_KEYS_MAP = {
|
||||
"Ed25519": Ed25519PrivateKey,
|
||||
"Ed448": Ed448PrivateKey,
|
||||
"X25519": X25519PrivateKey,
|
||||
"X448": X448PrivateKey,
|
||||
}
|
||||
|
||||
|
||||
class OKPKey(AsymmetricKey):
|
||||
"""Key class of the ``OKP`` key type."""
|
||||
|
||||
kty = "OKP"
|
||||
REQUIRED_JSON_FIELDS = ["crv", "x"]
|
||||
PUBLIC_KEY_FIELDS = REQUIRED_JSON_FIELDS
|
||||
PRIVATE_KEY_FIELDS = ["crv", "d"]
|
||||
PUBLIC_KEY_CLS = tuple(PUBLIC_KEYS_MAP.values())
|
||||
PRIVATE_KEY_CLS = tuple(PRIVATE_KEYS_MAP.values())
|
||||
SSH_PUBLIC_PREFIX = b"ssh-ed25519"
|
||||
|
||||
def exchange_shared_key(self, pubkey):
|
||||
# used in ECDHESAlgorithm
|
||||
private_key = self.get_private_key()
|
||||
if private_key and isinstance(private_key, (X25519PrivateKey, X448PrivateKey)):
|
||||
return private_key.exchange(pubkey)
|
||||
raise ValueError("Invalid key for exchanging shared key")
|
||||
|
||||
@staticmethod
|
||||
def get_key_curve(key):
|
||||
if isinstance(key, (Ed25519PublicKey, Ed25519PrivateKey)):
|
||||
return "Ed25519"
|
||||
elif isinstance(key, (Ed448PublicKey, Ed448PrivateKey)):
|
||||
return "Ed448"
|
||||
elif isinstance(key, (X25519PublicKey, X25519PrivateKey)):
|
||||
return "X25519"
|
||||
elif isinstance(key, (X448PublicKey, X448PrivateKey)):
|
||||
return "X448"
|
||||
|
||||
def load_private_key(self):
|
||||
crv_key = PRIVATE_KEYS_MAP[self._dict_data["crv"]]
|
||||
d_bytes = urlsafe_b64decode(to_bytes(self._dict_data["d"]))
|
||||
return crv_key.from_private_bytes(d_bytes)
|
||||
|
||||
def load_public_key(self):
|
||||
crv_key = PUBLIC_KEYS_MAP[self._dict_data["crv"]]
|
||||
x_bytes = urlsafe_b64decode(to_bytes(self._dict_data["x"]))
|
||||
return crv_key.from_public_bytes(x_bytes)
|
||||
|
||||
def dumps_private_key(self):
|
||||
obj = self.dumps_public_key(self.private_key.public_key())
|
||||
d_bytes = self.private_key.private_bytes(
|
||||
Encoding.Raw, PrivateFormat.Raw, NoEncryption()
|
||||
)
|
||||
obj["d"] = to_unicode(urlsafe_b64encode(d_bytes))
|
||||
return obj
|
||||
|
||||
def dumps_public_key(self, public_key=None):
|
||||
if public_key is None:
|
||||
public_key = self.public_key
|
||||
x_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
return {
|
||||
"crv": self.get_key_curve(public_key),
|
||||
"x": to_unicode(urlsafe_b64encode(x_bytes)),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, crv="Ed25519", options=None, is_private=False) -> "OKPKey":
|
||||
if crv not in PRIVATE_KEYS_MAP:
|
||||
raise ValueError(f'Invalid crv value: "{crv}"')
|
||||
private_key_cls = PRIVATE_KEYS_MAP[crv]
|
||||
raw_key = private_key_cls.generate()
|
||||
if not is_private:
|
||||
raw_key = raw_key.public_key()
|
||||
return cls.import_key(raw_key, options=options)
|
||||
@@ -0,0 +1,46 @@
|
||||
import binascii
|
||||
|
||||
from authlib.common.encoding import json_loads
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.encoding import urlsafe_b64decode
|
||||
from authlib.jose.errors import DecodeError
|
||||
|
||||
|
||||
def extract_header(header_segment, error_cls):
|
||||
if len(header_segment) > 256000:
|
||||
raise ValueError("Value of header is too long")
|
||||
|
||||
header_data = extract_segment(header_segment, error_cls, "header")
|
||||
|
||||
try:
|
||||
header = json_loads(header_data.decode("utf-8"))
|
||||
except ValueError as e:
|
||||
raise error_cls(f"Invalid header string: {e}") from e
|
||||
|
||||
if not isinstance(header, dict):
|
||||
raise error_cls("Header must be a json object")
|
||||
return header
|
||||
|
||||
|
||||
def extract_segment(segment, error_cls, name="payload"):
|
||||
if len(segment) > 256000:
|
||||
raise ValueError(f"Value of {name} is too long")
|
||||
|
||||
try:
|
||||
return urlsafe_b64decode(segment)
|
||||
except (TypeError, binascii.Error) as exc:
|
||||
msg = f"Invalid {name} padding"
|
||||
raise error_cls(msg) from exc
|
||||
|
||||
|
||||
def ensure_dict(s, structure_name):
|
||||
if not isinstance(s, dict):
|
||||
try:
|
||||
s = json_loads(to_unicode(s))
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise DecodeError(f"Invalid {structure_name}") from exc
|
||||
|
||||
if not isinstance(s, dict):
|
||||
raise DecodeError(f"Invalid {structure_name}")
|
||||
|
||||
return s
|
||||
Reference in New Issue
Block a user