Skip to content

Commit 5761117

Browse files
committed
py_vapid: Basic VAPID header generation based on PyPi py_vapid.
Signed-off-by: Jonah Bron <[email protected]>
1 parent 73af895 commit 5761117

File tree

3 files changed

+173
-0
lines changed

3 files changed

+173
-0
lines changed

Diff for: python-ecosys/py_vapid/manifest.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
metadata(
2+
version="0.1.0",
3+
pypi="py-vapid",
4+
author="Jonah Bron <[email protected]>",
5+
description="""
6+
VAPID
7+
""",
8+
)
9+
10+
require("pyjwt")
11+
12+
package("py_vapid")

Diff for: python-ecosys/py_vapid/py_vapid/__init__.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Based on https://github.com/web-push-libs/vapid
3+
"""
4+
5+
import binascii
6+
import time
7+
import jwt
8+
9+
from cryptography import serialization
10+
11+
12+
def _to_b64url(data):
13+
return (
14+
binascii.b2a_base64(data)
15+
.rstrip(b"\n")
16+
.rstrip(b"=")
17+
.replace(b"+", b"-")
18+
.replace(b"/", b"_")
19+
)
20+
21+
22+
class Vapid:
23+
def __init__(self, private_key):
24+
self._private_key = private_key
25+
26+
def sign(self, claims):
27+
claim = claims
28+
if "exp" not in claim:
29+
# Default to expiring 20 hours into the future (the max).
30+
# https://datatracker.ietf.org/doc/html/rfc8292#section-2
31+
exp = int(time.time()) + 86400
32+
# Correct the epoch offset if not the Unix standard.
33+
if time.gmtime(0)[0] == 2000:
34+
exp += 946684800 # Unix timestamp of 2000-01-01
35+
36+
claim["exp"] = exp
37+
38+
token = jwt.encode(claim, self._private_key, "ES256")
39+
public_key = _to_b64url(
40+
self._private_key.public_key().public_bytes(
41+
encoding=serialization.Encoding.X962,
42+
format=serialization.PublicFormat.UncompressedPoint,
43+
)
44+
).decode()
45+
46+
return {"Authorization": f"vapid t={token},k={public_key}"}
47+
48+
49+
# Re-export for interface compatibility with PyPi py-vapid
50+
Vapid02 = Vapid

Diff for: python-ecosys/py_vapid/test_vapid.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import jwt
2+
import py_vapid
3+
from time import time
4+
from cryptography import ec
5+
from machine import RTC
6+
7+
8+
"""
9+
Run tests by executing:
10+
11+
```
12+
mpremote fs cp py_vapid/__init__.py :lib/py_vapid.py + run test_vapid.py
13+
```
14+
15+
The [ucryptography](https://github.com/dmazzella/ucryptography) library must
16+
be present in the firmware for this library and tests to work.
17+
"""
18+
19+
rtc = RTC()
20+
21+
GOLDEN_0 = (
22+
0xEB6DFB26C7A3C23D33C60F7C7BA61B6893451F2643E0737B20759E457825EE75,
23+
(2010, 1, 1, 0, 0, 0, 0, 0),
24+
{
25+
"aud": "https://updates.push.services.mozilla.com",
26+
"sub": "mailto:[email protected]",
27+
"exp": 9876543,
28+
},
29+
"vapid t=eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFUzI1NiJ9.eyJhdWQiOiAiaHR0cHM6Ly91cGRhdGVzLnB1c2guc2VydmljZXMubW96aWxsYS5jb20iLCAic3ViIjogIm1haWx0bzphZG1pbkBleGFtcGxlLmNvbSIsICJleHAiOiA5ODc2NTQzfQ.DLB6PF2RApzk0n0oH-Kv_Onuwg9C7VXakM-GlEMCwj50rQ7G0hF_vLIYzCPeXT8Hu8Uup900YBapZ9y45vc8QA,k=BKoKs6nJ3466nCEQ5TvFkBIGBKSGplPTUBzJlLXM13I8S0SF-o_NSB-Q4At3BeLSrZVptEd5xBuGRXCKMe_YRg8",
30+
)
31+
32+
GOLDEN_1 = (
33+
0x4370082632776C74FDC5517AC12881413A60B25D10E863296AD67E4260A3BF56,
34+
(2015, 1, 1, 0, 0, 0, 0, 0),
35+
{
36+
"aud": "https://updates.push.services.mozilla.com",
37+
"sub": "mailto:[email protected]",
38+
},
39+
"vapid t=eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFUzI1NiJ9.eyJleHAiOiAxNDIwMTU2ODAwLCAic3ViIjogIm1haWx0bzphZG1pbkBleGFtcGxlLmNvbSIsICJhdWQiOiAiaHR0cHM6Ly91cGRhdGVzLnB1c2guc2VydmljZXMubW96aWxsYS5jb20ifQ.NlVtqjGWy-hvNtoScrwAv-4cpNYrgUJ4EVgtxTnIn-haPtBSpak7aQN518tVYelQB1TZqc0bxAjWfK9QvZUbOA,k=BGEwf7m9F3vCvOuPeN4pEZ91t-dpSmg_y8ZXMfOyl-f22zw10ho_4EeBqZj2-NtW_Kb98b6tGjOKO_-TJiWvyfo",
40+
)
41+
42+
# Set of opaquely known-good scenarios to check against
43+
golden_test_cases = [GOLDEN_0, GOLDEN_1]
44+
45+
46+
# Test basic validation of claim
47+
private_key_0 = ec.derive_private_key(
48+
0x5C76C15BBC541E7BF6987557124A6E6EB745723B1CF20E2ED2A3ED5B7C16DD46, ec.SECP256R1()
49+
)
50+
vapid = py_vapid.Vapid(private_key=private_key_0)
51+
rtc.datetime((2018, 1, 1, 0, 0, 0, 0, 0))
52+
headers = vapid.sign(
53+
{
54+
"aud": "https://fcm.googleapis.com",
55+
"sub": "mailto:[email protected]",
56+
"exp": 1493315200,
57+
}
58+
)
59+
60+
actual_token = headers["Authorization"].split(" ")[1].split(",")[0].split("=")[1]
61+
actual_decoded_claim = jwt.decode(actual_token, private_key_0.public_key(), "ES256")
62+
assert (
63+
actual_decoded_claim["aud"] == "https://fcm.googleapis.com"
64+
), f"Claim audience '{actual_decoded_claim['aud']}' does not match input"
65+
assert (
66+
actual_decoded_claim["sub"] == "mailto:[email protected]"
67+
), f"Claim subscriber '{actual_decoded_claim['sub']}' does not match input"
68+
assert (
69+
actual_decoded_claim["exp"] == 1493315200
70+
), f"Claim exp '{actual_decoded_claim['exp']}' does not match input"
71+
print(f"Test claim validation: Passed")
72+
73+
74+
# Test auto expiration date population
75+
private_key_1 = ec.derive_private_key(
76+
0x5C76C15BBC541E7BF6987557124A6E6EB745723B1CF20E2ED2A3ED5B7C16DD46, ec.SECP256R1()
77+
)
78+
vapid = py_vapid.Vapid(private_key=private_key_1)
79+
rtc.datetime((2017, 1, 1, 0, 0, 0, 0, 0))
80+
headers = vapid.sign(
81+
{
82+
"aud": "https://updates.push.services.mozilla.com",
83+
"sub": "mailto:[email protected]",
84+
}
85+
)
86+
87+
actual_token = headers["Authorization"].split(" ")[1].split(",")[0].split("=")[1]
88+
actual_decoded_claim = jwt.decode(actual_token, private_key_1.public_key(), "ES256")
89+
assert (
90+
actual_decoded_claim["exp"] == 1483315200
91+
), f"Claim exp '{actual_decoded_claim['exp']}' does not match expected 2017-01-02 value"
92+
print(f"Test auto expiry: Passed")
93+
94+
95+
# Because they provide the least information about what could have gone wrong,
96+
# Run golden test cases after all more specific tests pass first.
97+
for case_no, case in enumerate(golden_test_cases):
98+
private_key_number, curr_time, claim, expected_id = case
99+
try:
100+
private_key = ec.derive_private_key(private_key_number, ec.SECP256R1())
101+
vapid = py_vapid.Vapid(private_key=private_key)
102+
rtc.datetime(curr_time)
103+
headers = vapid.sign(claim)
104+
105+
assert (
106+
headers["Authorization"] == expected_id
107+
), f"Authorization header '{headers['Authorization']}' does not match golden test case {case_no}"
108+
print(f"Golden test case {case_no}: Passed")
109+
except Exception as e:
110+
print(f"Golden test case {case_no}: Failed")
111+
raise e

0 commit comments

Comments
 (0)