Repository: cal-itp/benefits

Issue:
Remove dependency on `jwcrypto` and `cryptography`
This task is to make the code changes necessary for removing `jwcrypto` from our `requirements.txt` file. (`cryptography` is a transitive dependency of `jwcrypto`, so we can remove that as well.) Some of our Django models currently return `jwk.JWK` instances in some of their methods.

## Acceptance Criteria
- [x] The `requirements.txt` file does not contain `jwcrypto` or any of its dependencies
- [x] The Benefits app still works as expected
- [x] Unit tests still pass

## Additional context
#141 mentions that a goal is to consolidate dependency management, which means `benefits` should not be directly depending on packages that are only needed for the Eligibility API. #596 pulls out most of the usage of `jwcrypto`, except for the ones left in `core/models.py` and `tests/pytests/eligibility/test_views.py`.



Discussion:
Commented on [cal-itp/eligibility-api#31](https://github.com/cal-itp/eligibility-api/pull/31#issuecomment-1136414944), which I think does more than necessary to solve this.

> My thinking for solving this issue was actually more to simplify the benefits side - why do the models even need to return that JWK type? They are just [passing directly into this API](https://github.com/cal-itp/benefits/blob/dev/benefits/eligibility/views.py#L190) - it should fully internalize the logic and deal with the raw underlying type used in the model (e.g. the text or the bytes).

> 
> We don't really need a special class here.

Resolving patch:
diff --git a/benefits/core/models.py b/benefits/core/models.py
--- a/benefits/core/models.py
+++ b/benefits/core/models.py
@@ -6,8 +6,6 @@
 from django.db import models
 from django.urls import reverse
 
-from jwcrypto import jwk
-
 
 logger = logging.getLogger(__name__)
 
@@ -22,12 +20,6 @@ class PemData(models.Model):
     def __str__(self):
         return self.label
 
-    @property
-    def jwk(self):
-        """jwcrypto.jwk.JWK instance from this PemData."""
-        pem_bytes = bytes(self.text, "utf-8")
-        return jwk.JWK.from_pem(pem_bytes)
-
 
 class AuthProvider(models.Model):
     """An entity that provides authentication for eligibility verifiers."""
@@ -100,9 +92,9 @@ def __str__(self):
         return self.name
 
     @property
-    def public_jwk(self):
-        """jwcrypto.jwk.JWK instance of this Verifier's public key"""
-        return self.public_key.jwk
+    def public_key_data(self):
+        """This Verifier's public key as a string."""
+        return self.public_key.text
 
     @property
     def requires_authentication(self):
@@ -189,9 +181,9 @@ def index_url(self):
         return reverse("core:agency_index", args=[self.slug])
 
     @property
-    def private_jwk(self):
-        """jwcrypto.jwk.JWK instance of this Agency's private key"""
-        return self.private_key.jwk
+    def private_key_data(self):
+        """This Agency's private key as a string."""
+        return self.private_key.text
 
     @staticmethod
     def by_id(id):
diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py
--- a/benefits/eligibility/views.py
+++ b/benefits/eligibility/views.py
@@ -193,10 +193,10 @@ def _verify(request, form):
         issuer=settings.ALLOWED_HOSTS[0],
         agency=agency.agency_id,
         jws_signing_alg=agency.jws_signing_alg,
-        client_private_jwk=agency.private_jwk,
+        client_private_key=agency.private_key_data,
         jwe_encryption_alg=verifier.jwe_encryption_alg,
         jwe_cek_enc=verifier.jwe_cek_enc,
-        server_public_jwk=verifier.public_jwk,
+        server_public_key=verifier.public_key_data,
     )
 
     # get the eligibility type names


Test patch:
diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py
--- a/tests/pytest/eligibility/test_views.py
+++ b/tests/pytest/eligibility/test_views.py
@@ -8,7 +8,6 @@
 import uuid
 
 from pathlib import Path
-from jwcrypto import jwk
 
 from benefits.core import session
 from benefits.core.models import TransitAgency
@@ -88,10 +87,10 @@ def test_confirm_success(mocker, rf):
                 "eligibility": ["type1"],
             },
             verifier.jws_signing_alg,
-            _get_jwk("server.key"),
+            _get_key("server.key"),
             verifier.jwe_encryption_alg,
             verifier.jwe_cek_enc,
-            _get_jwk("client.pub"),
+            _get_key("client.pub"),
         ),
     )
 
@@ -108,12 +107,12 @@ def test_confirm_success(mocker, rf):
     assert response.url == reverse("enrollment:index")
 
 
-def _get_jwk(filename):
+def _get_key(filename):
     current_path = Path(os.path.dirname(os.path.realpath(__file__)))
     file_path = current_path / "keys" / filename
 
     with file_path.open(mode="rb") as pemfile:
-        key = jwk.JWK.from_pem(pemfile.read())
+        key = str(pemfile.read(), "utf-8")
 
     return key
 
@@ -190,10 +189,10 @@ def _tokenize_response_error_scenarios():
                     "eligibility": ["type1"],
                 },
                 "RS512",  # signing algorithm that doesn't match verifier.jws_signing_alg
-                _get_jwk("server.key"),
+                _get_key("server.key"),
                 verifier.jwe_encryption_alg,
                 verifier.jwe_cek_enc,
-                _get_jwk("client.pub"),
+                _get_key("client.pub"),
             ),
             id='TokenError("JWS token signature verification failed")',
         ),