From fcdc7d76b5a5924f9343a92b2627944a855ae62a Mon Sep 17 00:00:00 2001 From: spilikin <108782+spilikin@users.noreply.github.com> Date: Mon, 10 Oct 2022 21:07:08 +0200 Subject: [PATCH] Add Brainpool EC-curves support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the support of Brainpool curves to jwcrypto. The Brainpool curves defined in RFC 5639 are mandatory for use in german e-health systems as defined by the Federal Office of Information Security (BSI) and National Digital Health Agency (gematik GmbH). In order to use the public E-Health APIs clients are required to: * Load and use the Brainpool keys using JWK * Sign and verify the signatures using the Brainpool elliptic curves using JWS * Encrypt and decrypt the data using the Brainpool elliptic curves and AES using JWE At the time of this commit there is no official standardization of these algorithms for JOSE/JWK/JWS/JWE. The use of these algorithms is specified solely by the gematik GmbH – National Digital Health Agency - for use in german e-health applications. Signed-off-by: Sergej Suskov --- jwcrypto/jwa.py | 53 +++++++++++++++++++++++- jwcrypto/jwk.py | 23 ++++++++++- jwcrypto/tests.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 3 deletions(-) diff --git a/jwcrypto/jwa.py b/jwcrypto/jwa.py index 854a116..10102b1 100644 --- a/jwcrypto/jwa.py +++ b/jwcrypto/jwa.py @@ -1053,6 +1053,54 @@ class _A256Gcm(_AesGcm, JWAAlgorithm): algorithm_use = 'enc' +class _BP256R1(_RawEC, JWAAlgorithm): + + name = "BP256R1" + description = ( + "ECDSA using Brainpool256R1 curve and SHA-256" + " (unregistered, custom-defined in breach" + " of IETF rules by gematik GmbH)" + ) + keysize = 256 + algorithm_usage_location = 'alg' + algorithm_use = 'sig' + + def __init__(self): + super(_BP256R1, self).__init__('BP-256', hashes.SHA256()) + + +class _BP384R1(_RawEC, JWAAlgorithm): + + name = "BP384R1" + description = ( + "ECDSA using Brainpool384R1 curve and SHA-384" + " (unregistered, custom-defined in breach" + " of IETF rules by gematik GmbH)" + ) + keysize = 384 + algorithm_usage_location = 'alg' + algorithm_use = 'sig' + + def __init__(self): + super(_BP384R1, self).__init__('BP-384', hashes.SHA384()) + + +class _BP512R1(_RawEC, JWAAlgorithm): + + name = "BP512R1" + description = ( + "ECDSA using Brainpool512R1 curve and SHA-512" + " (unregistered, custom-defined in breach" + " of IETF rules by gematik GmbH)" + ) + keysize = 512 + algorithm_usage_location = 'alg' + algorithm_use = 'sig' + + def __init__(self): + super(_BP512R1, self).__init__('BP-512', hashes.SHA512()) + + class JWA: """JWA Signing Algorithms. @@ -1097,7 +1145,10 @@ class JWA: 'A256CBC-HS512': _A256CbcHs512, 'A128GCM': _A128Gcm, 'A192GCM': _A192Gcm, - 'A256GCM': _A256Gcm + 'A256GCM': _A256Gcm, + 'BP256R1': _BP256R1, + 'BP384R1': _BP384R1, + 'BP512R1': _BP512R1 } @classmethod diff --git a/jwcrypto/jwk.py b/jwcrypto/jwk.py index 2357cba..181cd76 100644 --- a/jwcrypto/jwk.py +++ b/jwcrypto/jwk.py @@ -162,7 +162,17 @@ class ParmType(Enum): 'Ed25519': 'Ed25519 signature algorithm key pairs', 'Ed448': 'Ed448 signature algorithm key pairs', 'X25519': 'X25519 function key pairs', - 'X448': 'X448 function key pairs'} + 'X448': 'X448 function key pairs', + 'BP-256': 'BrainpoolP256R1 curve' + ' (unregistered, custom-defined in breach' + ' of IETF rules by gematik GmbH)', + 'BP-384': 'BrainpoolP384R1 curve' + ' (unregistered, custom-defined in breach' + ' of IETF rules by gematik GmbH)', + 'BP-512': 'BrainpoolP512R1 curve' + ' (unregistered, custom-defined in breach' + ' of IETF rules by gematik GmbH)' + } """Registry of allowed Elliptic Curves""" # RFC 7517 - 8.2 @@ -186,7 +196,10 @@ class ParmType(Enum): JWKpycaCurveMap = {'secp256r1': 'P-256', 'secp384r1': 'P-384', 'secp521r1': 'P-521', - 'secp256k1': 'secp256k1'} + 'secp256k1': 'secp256k1', + 'brainpoolP256r1': 'BP-256', + 'brainpoolP384r1': 'BP-384', + 'brainpoolP512r1': 'BP-512'} IANANamedInformationHashAlgorithmRegistry = { 'sha-256': hashes.SHA256(), @@ -453,6 +466,12 @@ def _get_curve_by_name(self, name, ctype=None): return ec.SECP521R1() elif cname == 'secp256k1': return ec.SECP256K1() + elif cname == 'BP-256': + return ec.BrainpoolP256R1() + elif cname == 'BP-384': + return ec.BrainpoolP384R1() + elif cname == 'BP-512': + return ec.BrainpoolP512R1() elif cname in _OKP_CURVES_TABLE: return _OKP_CURVES_TABLE[cname] else: diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py index ece632f..6f04c2a 100644 --- a/jwcrypto/tests.py +++ b/jwcrypto/tests.py @@ -294,7 +294,66 @@ "x": "Ss6na3mcci8Ud4lQrjaB_T40sfKApEcl2RLIWOJdjow", "y": "7l9qIKtKPW6oEiOYBt7r22Sm0mtFJU-yBkkvMvpscd8", "d": "GYhU2vrYGZrjLZn71Xniqm54Mi53xiYtaTLawzaf9dA" + } + ] +} + +PublicKeys_brainpool = { + "keys": [ + { + "kty": "EC", + "crv": "BP-256", + "x": "mpkJ29_CYAD0mzQ_MsrbjFMFYtcc9Oxpro37Fa4cLfI", + "y": "iBfhNHk0cI73agNpjbKW62dvuVxn7kxp1Sm8oDnzHl8", }, + { + "kty": "EC", + "crv": "BP-384", + "x": ("WZanneaC2Hi3xslA4znJv7otyEdV5dTPzNUvBjBXPM" + "ytf4mRY9JaAITdItjvUTAh"), + "y": ("KNLRTNdvUg66aB_TVW4POZkE3q8S0YoQrCzYUrExRDe" + "_BXikkqIama-GYQ3UBOQL"), + }, + { + "kty": "EC", + "crv": "BP-512", + "x": ("aQXpvz7DH9OK5eFNO9dY3BdPY1v0-8Rg9KC322PY1Jy" + "BJq3EhT0uR_-tgbL2E_aGP6k56lF1xIOOtQxo8zziGA"), + "y": ("l9XLHHncigOPr5Tvnj_mVzBFv6i7rdBQrLTq3RXZlCC" + "_f_q6L2o79K9IrN_J2wWxAfS8ekuGPGlHZUzK-3D9sA"), + } + ] +} + +PrivateKeys_brainpool = { + "keys": [ + { + "kty": "EC", + "crv": "BP-256", + "x": "mpkJ29_CYAD0mzQ_MsrbjFMFYtcc9Oxpro37Fa4cLfI", + "y": "iBfhNHk0cI73agNpjbKW62dvuVxn7kxp1Sm8oDnzHl8", + "d": "KdKRgq0WEM97BQw3jpW_fTOep6fn-Samv4DfDNb-4s4" + }, + { + "kty": "EC", + "crv": "BP-384", + "x": ("WZanneaC2Hi3xslA4znJv7otyEdV5dTPzNUvBjBXPM" + "ytf4mRY9JaAITdItjvUTAh"), + "y": ("KNLRTNdvUg66aB_TVW4POZkE3q8S0YoQrCzYUrExRDe" + "_BXikkqIama-GYQ3UBOQL"), + "d": ("B5WeRV0-RztAPAhRbphSAUrsIzy-eSfWGSM5FxOQGlJ" + "cq-ECLA_-SIlH7NdWIEJY") + }, + { + "kty": "EC", + "crv": "BP-512", + "x": ("aQXpvz7DH9OK5eFNO9dY3BdPY1v0-8Rg9KC322PY1Jy" + "BJq3EhT0uR_-tgbL2E_aGP6k56lF1xIOOtQxo8zziGA"), + "y": ("l9XLHHncigOPr5Tvnj_mVzBFv6i7rdBQrLTq3RXZlCC" + "_f_q6L2o79K9IrN_J2wWxAfS8ekuGPGlHZUzK-3D9sA"), + "d": ("F_LJ9rebAjOtxoMUfngIywYsnJlZNjy3gxNAEvHjSkL" + "m6RUUdLXDwc50EMp0LeTh1ku039D5kldK3S9Xi0yKZA") + } ] } @@ -385,6 +444,15 @@ def test_generate_EC_key(self): # New secp256k curve key = jwk.JWK.generate(kty='EC', curve='secp256k1') key.get_op_key('verify', 'secp256k1') + # Brainpool256R1 curve + key = jwk.JWK.generate(kty='EC', crv='BP-256') + key.get_op_key('verify', 'BP-256') + # Brainpool384R1 curve + key = jwk.JWK.generate(kty='EC', crv='BP-384') + key.get_op_key('verify', 'BP-384') + # Brainpool256R1 curve + key = jwk.JWK.generate(kty='EC', crv='BP-512') + key.get_op_key('verify', 'BP-512') def test_generate_OKP_keys(self): for crv in jwk.ImplementedOkpCurves: @@ -576,6 +644,16 @@ def test_create_priKeys_secp256k1(self): for key in keylist: jwk.JWK(**key) + def test_create_pubKeys_brainpool(self): + keylist = PublicKeys_brainpool['keys'] + for key in keylist: + jwk.JWK(**key) + + def test_create_priKeys_brainpool(self): + keylist = PrivateKeys_brainpool['keys'] + for key in keylist: + jwk.JWK(**key) + def test_thumbprint_eddsa(self): for i in range(0, len(PublicKeys_EdDsa['keys'])): k = jwk.JWK(**PublicKeys_EdDsa['keys'][i]) @@ -987,6 +1065,31 @@ def test_secp256k1_signing_and_verification(self): jws_verify.verify(key.public()) self.assertEqual(jws_verify.payload, payload) + def test_brainpool_signing_and_verification(self): + for key_data in PrivateKeys_brainpool['keys']: + key = jwk.JWK(**key_data) + payload = bytes(bytearray(A1_payload)) + jws_test = jws.JWS(payload) + + curve_name = key.get('crv') + if curve_name == "BP-256": + alg = "BP256R1" + elif curve_name == "BP-384": + alg = "BP384R1" + else: + alg = "BP512R1" + + jws_test.allowed_algs = [alg] + jws_test.add_signature(key, None, json_encode({"alg": alg}), None) + jws_test_serialization_compact = jws_test.serialize(compact=True) + + jws_verify = jws.JWS() + jws_verify.allowed_algs = [alg] + jws_verify.deserialize(jws_test_serialization_compact) + jws_verify.verify(key.public()) + + self.assertEqual(jws_verify.payload, payload) + def test_jws_issue_224(self): key = jwk.JWK().generate(kty='oct')