File support-new-cryptography.patch of Package python-py-vapid

From 4898bb4d418c4b3b3a0bb3620cb4cb13a481e0ba Mon Sep 17 00:00:00 2001
From: jrconlin <jr+git@mozilla.com>
Date: Thu, 2 Oct 2025 10:29:35 -0700
Subject: [PATCH 1/2] feat: Update to latest EC recommendations

Closes #105
---
 python/py_vapid/__init__.py | 116 +++++++++++++++++-------------------
 python/pyproject.toml       |   2 +-
 2 files changed, 57 insertions(+), 61 deletions(-)

diff --git a/py_vapid/__init__.py b/py_vapid/__init__.py
index 4a6eff2..94d1113 100644
--- a/py_vapid/__init__.py
+++ b/py_vapid/__init__.py
@@ -25,6 +25,7 @@
 
 class VapidException(Exception):
     """An exception wrapper for Vapid."""
+
     pass
 
 
@@ -34,6 +35,7 @@ class Vapid01(object):
     https://tools.ietf.org/html/draft-ietf-webpush-vapid-01
 
     """
+
     _private_key = None
     _public_key = None
     _schema = "WebPush"
@@ -65,14 +67,14 @@ def from_raw(cls, private_raw):
         key = ec.derive_private_key(
             int(binascii.hexlify(b64urldecode(private_raw)), 16),
             curve=ec.SECP256R1(),
-            backend=default_backend())
+            backend=default_backend(),
+        )
         return cls(key)
 
     @classmethod
     def from_raw_public(cls, public_raw):
         key = ec.EllipticCurvePublicKey.from_encoded_point(
-            curve=ec.SECP256R1(),
-            data=b64urldecode(public_raw)
+            curve=ec.SECP256R1(), data=b64urldecode(public_raw)
         )
         ss = cls()
         ss._public_key = key
@@ -87,8 +89,7 @@ def from_pem(cls, private_key):
 
         """
         # not sure why, but load_pem_private_key fails to deserialize
-        return cls.from_der(
-            b''.join(private_key.splitlines()[1:-1]))
+        return cls.from_der(b"".join(private_key.splitlines()[1:-1]))
 
     @classmethod
     def from_der(cls, private_key):
@@ -98,9 +99,9 @@ def from_der(cls, private_key):
         :type private_key: bytes
 
         """
-        key = serialization.load_der_private_key(b64urldecode(private_key),
-                                                 password=None,
-                                                 backend=default_backend())
+        key = serialization.load_der_private_key(
+            b64urldecode(private_key), password=None, backend=default_backend()
+        )
         return cls(key)
 
     @classmethod
@@ -118,13 +119,13 @@ def from_file(cls, private_key_file=None):
             vapid.generate_keys()
             vapid.save_key(private_key_file)
             return vapid
-        with open(private_key_file, 'r') as file:
+        with open(private_key_file, "r") as file:
             private_key = file.read()
         try:
             if "-----BEGIN" in private_key:
-                vapid = cls.from_pem(private_key.encode('utf8'))
+                vapid = cls.from_pem(private_key.encode("utf8"))
             else:
-                vapid = cls.from_der(private_key.encode('utf8'))
+                vapid = cls.from_der(private_key.encode("utf8"))
             return vapid
         except Exception as exc:
             logging.error("Could not open private key file: %s", repr(exc))
@@ -156,11 +157,10 @@ def verify(cls, key, auth):
         type key: str
 
         """
-        tokens = auth.rsplit(' ', 1)[1].rsplit('.', 1)
+        tokens = auth.rsplit(" ", 1)[1].rsplit(".", 1)
         kp = cls().from_raw_public(key.encode())
         return kp.verify_token(
-            validation_token=tokens[0].encode(),
-            verification_token=tokens[1]
+            validation_token=tokens[0].encode(), verification_token=tokens[1]
         )
 
     @property
@@ -197,20 +197,19 @@ def public_key(self):
 
     def generate_keys(self):
         """Generate a valid ECDSA Key Pair."""
-        self.private_key = ec.generate_private_key(ec.SECP256R1,
-                                                   default_backend())
+        self.private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
 
     def private_pem(self):
         return self.private_key.private_bytes(
             encoding=serialization.Encoding.PEM,
             format=serialization.PrivateFormat.PKCS8,
-            encryption_algorithm=serialization.NoEncryption()
+            encryption_algorithm=serialization.NoEncryption(),
         )
 
     def public_pem(self):
         return self.public_key.public_bytes(
             encoding=serialization.Encoding.PEM,
-            format=serialization.PublicFormat.SubjectPublicKeyInfo
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
         )
 
     def save_key(self, key_file):
@@ -245,14 +244,14 @@ def verify_token(self, validation_token, verification_token):
         :rtype: boolean
 
         """
-        hsig = b64urldecode(verification_token.encode('utf8'))
+        hsig = b64urldecode(verification_token.encode("utf8"))
         r = int(binascii.hexlify(hsig[:32]), 16)
         s = int(binascii.hexlify(hsig[32:]), 16)
         try:
             self.public_key.verify(
                 ecutils.encode_dss_signature(r, s),
                 validation_token,
-                signature_algorithm=ec.ECDSA(hashes.SHA256())
+                signature_algorithm=ec.ECDSA(hashes.SHA256()),
             )
             return True
         except InvalidSignature:
@@ -260,23 +259,25 @@ def verify_token(self, validation_token, verification_token):
 
     def _base_sign(self, claims):
         cclaims = copy.deepcopy(claims)
-        if not cclaims.get('exp'):
-            cclaims['exp'] = int(time.time()) + 86400
-        if not self.conf.get('no-strict', False):
-            valid = _check_sub(cclaims.get('sub', ''))
+        if not cclaims.get("exp"):
+            cclaims["exp"] = int(time.time()) + 86400
+        if not self.conf.get("no-strict", False):
+            valid = _check_sub(cclaims.get("sub", ""))
         else:
-            valid = cclaims.get('sub') is not None
+            valid = cclaims.get("sub") is not None
         if not valid:
             raise VapidException(
                 "Missing 'sub' from claims. "
-                "'sub' is your admin email as a mailto: link.")
-        if not re.match(r"^https?://[^/:]+(:\d+)?$",
-                        cclaims.get("aud", ""),
-                        re.IGNORECASE):
+                "'sub' is your admin email as a mailto: link."
+            )
+        if not re.match(
+            r"^https?://[^/:]+(:\d+)?$", cclaims.get("aud", ""), re.IGNORECASE
+        ):
             raise VapidException(
                 "Missing 'aud' from claims. "
                 "'aud' is the scheme, host and optional port for this "
-                "transaction e.g. https://example.com:8080")
+                "transaction e.g. https://example.com:8080"
+            )
         return cclaims
 
     def sign(self, claims, crypto_key=None):
@@ -292,19 +293,22 @@ def sign(self, claims, crypto_key=None):
 
         """
         sig = sign(self._base_sign(claims), self.private_key)
-        pkey = 'p256ecdsa='
+        pkey = "p256ecdsa="
         pkey += b64urlencode(
             self.public_key.public_bytes(
                 serialization.Encoding.X962,
-                serialization.PublicFormat.UncompressedPoint
-            ))
+                serialization.PublicFormat.UncompressedPoint,
+            )
+        )
         if crypto_key:
-            crypto_key = crypto_key + ';' + pkey
+            crypto_key = crypto_key + ";" + pkey
         else:
             crypto_key = pkey
 
-        return {"Authorization": "{} {}".format(self._schema, sig.strip('=')),
-                "Crypto-Key": crypto_key}
+        return {
+            "Authorization": "{} {}".format(self._schema, sig.strip("=")),
+            "Crypto-Key": crypto_key,
+        }
 
 
 class Vapid02(Vapid01):
@@ -313,6 +317,7 @@ class Vapid02(Vapid01):
     https://tools.ietf.org/html/rfc8292
 
     """
+
     _schema = "vapid"
 
     def sign(self, claims, crypto_key=None):
@@ -329,14 +334,11 @@ def sign(self, claims, crypto_key=None):
         """
         sig = sign(self._base_sign(claims), self.private_key)
         pkey = self.public_key.public_bytes(
-                serialization.Encoding.X962,
-                serialization.PublicFormat.UncompressedPoint
-            )
-        return{
+            serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
+        )
+        return {
             "Authorization": "{schema} t={t},k={k}".format(
-                schema=self._schema,
-                t=sig,
-                k=b64urlencode(pkey)
+                schema=self._schema, t=sig, k=b64urlencode(pkey)
             )
         }
 
@@ -349,27 +351,23 @@ def verify(cls, auth):
         :rtype: bool
 
         """
-        pref_tok = auth.rsplit(' ', 1)
-        assert pref_tok[0].lower() == cls._schema, (
-                "Incorrect schema specified")
+        pref_tok = auth.rsplit(" ", 1)
+        assert pref_tok[0].lower() == cls._schema, "Incorrect schema specified"
         parts = {}
-        for tok in pref_tok[1].split(','):
-            kv = tok.split('=', 1)
+        for tok in pref_tok[1].split(","):
+            kv = tok.split("=", 1)
             parts[kv[0]] = kv[1]
-        assert 'k' in parts.keys(), (
-                "Auth missing public key 'k' value")
-        assert 't' in parts.keys(), (
-                "Auth missing token set 't' value")
-        kp = cls().from_raw_public(parts['k'].encode())
-        tokens = parts['t'].rsplit('.', 1)
+        assert "k" in parts.keys(), "Auth missing public key 'k' value"
+        assert "t" in parts.keys(), "Auth missing token set 't' value"
+        kp = cls().from_raw_public(parts["k"].encode())
+        tokens = parts["t"].rsplit(".", 1)
         return kp.verify_token(
-            validation_token=tokens[0].encode(),
-            verification_token=tokens[1]
+            validation_token=tokens[0].encode(), verification_token=tokens[1]
         )
 
 
 def _check_sub(sub):
-    """ Check to see if the `sub` is a properly formatted `mailto:`
+    """Check to see if the `sub` is a properly formatted `mailto:`
 
     a `mailto:` should be a SMTP mail address. Mind you, since I run
     YouFailAtEmail.com, you have every right to yell about how terrible
@@ -382,9 +380,7 @@ def _check_sub(sub):
     :rtype: bool
 
     """
-    pattern = (
-        r"^(mailto:.+@((localhost|[%\w-]+(\.[%\w-]+)+|([0-9a-f]{1,4}):+([0-9a-f]{1,4})?)))|https:\/\/(localhost|[\w-]+\.[\w\.-]+|([0-9a-f]{1,4}:+)+([0-9a-f]{1,4})?)$" # noqa
-        )
+    pattern = r"^(mailto:.+@((localhost|[%\w-]+(\.[%\w-]+)+|([0-9a-f]{1,4}):+([0-9a-f]{1,4})?)))|https:\/\/(localhost|[\w-]+\.[\w\.-]+|([0-9a-f]{1,4}:+)+([0-9a-f]{1,4})?)$"  # noqa
     return re.match(pattern, sub, re.IGNORECASE) is not None
 
openSUSE Build Service is sponsored by