55import os
66import logging
77import binascii
8- import base64
98import time
10- import hashlib
9+ import re
1110
12- import ecdsa
13- from jose import jws
11+ from cryptography .hazmat .backends import default_backend
12+ from cryptography .hazmat .primitives .asymmetric import ec
13+ from cryptography .hazmat .primitives import serialization
1414
15- # Show compliance version. For earlier versions see previously tagged releases.
16- VERSION = "VAPID-DRAFT-02/ECE-DRAFT-07"
17-
18-
19- def b64urldecode (data ):
20- """Decodes an unpadded Base64url-encoded string."""
21- return base64 .urlsafe_b64decode (data + "====" [:len (data ) % 4 ])
15+ from cryptography .hazmat .primitives import hashes
2216
17+ from py_vapid .utils import b64urldecode , b64urlencode
18+ from py_vapid .jwt import sign
2319
24- def b64urlencode (bstring ):
25- return binascii .b2a_base64 (
26- bstring ).decode ('utf8' ).replace ('\n ' , '' ).replace (
27- '+' , '-' ).replace ('/' , '_' ).replace ('=' , '' )
20+ # Show compliance version. For earlier versions see previously tagged releases.
21+ VERSION = "VAPID-DRAFT-02/ECE-DRAFT-07"
2822
2923
3024class VapidException (Exception ):
@@ -40,8 +34,6 @@ class Vapid01(object):
4034 """
4135 _private_key = None
4236 _public_key = None
43- _curve = ecdsa .NIST256p
44- _hasher = hashlib .sha256
4537 _schema = "WebPush"
4638
4739 def __init__ (self , private_key = None ):
@@ -52,41 +44,48 @@ def __init__(self, private_key=None):
5244
5345 """
5446 self .private_key = private_key
47+ if private_key :
48+ self ._public_key = self .private_key .public_key ()
5549
5650 @classmethod
57- def from_raw (cls , private_key ):
51+ def from_raw (cls , private_raw ):
5852 """Initialize VAPID using a private key point in "raw" or
59- "uncompressed" form.
53+ "uncompressed" form. Raw keys consist of a single, 32 octet
54+ encoded integer.
6055
61- :param private_key : A private key point in uncompressed form.
62- :type private_key: str
56+ :param private_raw : A private key point in uncompressed form.
57+ :type private_raw: bytes
6358
6459 """
65- key = ecdsa .SigningKey .from_string (b64urldecode (private_key ),
66- curve = cls ._curve ,
67- hashfunc = cls ._hasher )
60+ key = ec .derive_private_key (
61+ int (binascii .hexlify (b64urldecode (private_raw )), 16 ),
62+ curve = ec .SECP256R1 (),
63+ backend = default_backend ())
6864 return cls (key )
6965
7066 @classmethod
7167 def from_pem (cls , private_key ):
7268 """Initialize VAPID using a private key in PEM format.
7369
7470 :param private_key: A private key in PEM format.
75- :type private_key: str
71+ :type private_key: bytes
7672
7773 """
78- key = ecdsa .SigningKey .from_pem (private_key )
79- return cls (key )
74+ # not sure why, but load_pem_private_key fails to deserialize
75+ return cls .from_der (
76+ b'' .join (private_key .splitlines ()[1 :- 1 ]))
8077
8178 @classmethod
8279 def from_der (cls , private_key ):
8380 """Initialize VAPID using a private key in DER format.
8481
8582 :param private_key: A private key in DER format and Base64-encoded.
86- :type private_key: str
83+ :type private_key: bytes
8784
8885 """
89- key = ecdsa .SigningKey .from_der (base64 .b64decode (private_key ))
86+ key = serialization .load_der_private_key (b64urldecode (private_key ),
87+ password = None ,
88+ backend = default_backend ())
9089 return cls (key )
9190
9291 @classmethod
@@ -100,54 +99,69 @@ def from_file(cls, private_key_file=None):
10099 """
101100 if not os .path .isfile (private_key_file ):
102101 vapid = cls ()
102+ vapid .generate_keys ()
103103 vapid .save_key (private_key_file )
104104 return vapid
105105 private_key = open (private_key_file , 'r' ).read ()
106- vapid = None
107106 try :
108- if "BEGIN EC " in private_key :
109- vapid = cls .from_pem (private_key )
107+ if "----- BEGIN" in private_key :
108+ vapid = cls .from_pem (private_key . encode ( 'utf8' ) )
110109 else :
111- vapid = cls .from_der (private_key )
110+ vapid = cls .from_der (private_key .encode ('utf8' ))
111+ return vapid
112112 except Exception as exc :
113113 logging .error ("Could not open private key file: %s" , repr (exc ))
114114 raise VapidException (exc )
115- return vapid
116115
117116 @property
118117 def private_key (self ):
119118 """The VAPID private ECDSA key"""
120119 if not self ._private_key :
121- raise VapidException (
122- "No private key defined. Please import or generate a key." )
120+ raise VapidException ("No private key. Call generate_keys()" )
123121 return self ._private_key
124122
125123 @private_key .setter
126124 def private_key (self , value ):
127125 """Set the VAPID private ECDSA key
128126
129127 :param value: the byte array containing the private ECDSA key data
130- :type value: bytes
128+ :type value: ec.EllipticCurvePrivateKey
131129
132130 """
133131 self ._private_key = value
134- self ._public_key = None
132+ if value :
133+ self ._public_key = self .private_key .public_key ()
135134
136135 @property
137136 def public_key (self ):
138137 """The VAPID public ECDSA key
139138
140139 The public key is currently read only. Set it via the `.private_key`
141- method.
140+ method. This will autogenerate a public and private key if no value
141+ has been set.
142+
143+ :returns ec.EllipticCurvePublicKey
142144
143145 """
144- if not self ._public_key :
145- self ._public_key = self .private_key .get_verifying_key ()
146146 return self ._public_key
147147
148148 def generate_keys (self ):
149149 """Generate a valid ECDSA Key Pair."""
150- self .private_key = ecdsa .SigningKey .generate (curve = self ._curve )
150+ self .private_key = ec .generate_private_key (ec .SECP256R1 ,
151+ default_backend ())
152+
153+ def private_pem (self ):
154+ return self .private_key .private_bytes (
155+ encoding = serialization .Encoding .PEM ,
156+ format = serialization .PrivateFormat .PKCS8 ,
157+ encryption_algorithm = serialization .NoEncryption ()
158+ )
159+
160+ def public_pem (self ):
161+ return self .public_key .public_bytes (
162+ encoding = serialization .Encoding .PEM ,
163+ format = serialization .PublicFormat .SubjectPublicKeyInfo
164+ )
151165
152166 def save_key (self , key_file ):
153167 """Save the private key to a PEM file.
@@ -156,11 +170,9 @@ def save_key(self, key_file):
156170 :type key_file: str
157171
158172 """
159- file = open (key_file , "wb" )
160- if not self ._private_key :
161- self .generate_keys ()
162- file .write (self ._private_key .to_pem ())
163- file .close ()
173+ with open (key_file , "wb" ) as file :
174+ file .write (self .private_pem ())
175+ file .close ()
164176
165177 def save_public_key (self , key_file ):
166178 """Save the public key to a PEM file.
@@ -169,7 +181,7 @@ def save_public_key(self, key_file):
169181
170182 """
171183 with open (key_file , "wb" ) as file :
172- file .write (self .public_key . to_pem ())
184+ file .write (self .public_pem ())
173185 file .close ()
174186
175187 def validate (self , validation_token ):
@@ -183,8 +195,8 @@ def validate(self, validation_token):
183195 """
184196 sig = self .private_key .sign (
185197 validation_token ,
186- hashfunc = self . _hasher )
187- verification_token = base64 . urlsafe_b64encode (sig )
198+ signature_algorithm = ec . ECDSA ( hashes . SHA256 ()) )
199+ verification_token = b64urlencode (sig )
188200 return verification_token
189201
190202 def verify_token (self , validation_token , verification_token ):
@@ -198,17 +210,30 @@ def verify_token(self, validation_token, verification_token):
198210 :rtype: boolean
199211
200212 """
201- hsig = base64 .urlsafe_b64decode (verification_token )
202- return self .public_key .verify (hsig , validation_token ,
203- hashfunc = self ._hasher )
213+ hsig = b64urldecode (verification_token .encode ('utf8' ))
214+ return self .public_key .verify (
215+ hsig ,
216+ validation_token ,
217+ signature_algorithm = ec .ECDSA (hashes .SHA256 ())
218+ )
204219
205220 def _base_sign (self , claims ):
206221 if not claims .get ('exp' ):
207222 claims ['exp' ] = str (int (time .time ()) + 86400 )
208- if not claims .get ('sub' ):
223+ if not re .match ("mailto:.+@.+\..+" ,
224+ claims .get ('sub' , '' ),
225+ re .IGNORECASE ):
209226 raise VapidException (
210227 "Missing 'sub' from claims. "
211228 "'sub' is your admin email as a mailto: link." )
229+ if not re .match ("^https?:\/\/[^\/\.:]+\.[^\/:]+(:\d+)?$" ,
230+ claims .get ("aud" , "" ),
231+ re .IGNORECASE ):
232+ raise VapidException (
233+ "Missing 'aud' from claims. "
234+ "'aud' is the scheme, host and optional port for this "
235+ "transaction e.g. https://example.com:8080" )
236+
212237 return claims
213238
214239 def sign (self , claims , crypto_key = None ):
@@ -224,12 +249,10 @@ def sign(self, claims, crypto_key=None):
224249
225250 """
226251 claims = self ._base_sign (claims )
227- sig = jws . sign (claims , self .private_key , algorithm = "ES256" )
252+ sig = sign (claims , self .private_key )
228253 pkey = 'p256ecdsa='
229- pubkey = self .public_key .to_string ()
230- if len (pubkey ) == 64 :
231- pubkey = b'\04 ' + pubkey
232- pkey += b64urlencode (pubkey )
254+ pkey += b64urlencode (
255+ self .public_key .public_numbers ().encode_point ())
233256 if crypto_key :
234257 crypto_key = crypto_key + ';' + pkey
235258 else :
@@ -249,11 +272,8 @@ class Vapid02(Vapid01):
249272
250273 def sign (self , claims , crypto_key = None ):
251274 claims = self ._base_sign (claims )
252- sig = jws .sign (claims , self .private_key , algorithm = "ES256" )
253- pkey = self .public_key .to_string ()
254- # Make sure that the key is properly prefixed.
255- if len (pkey ) == 64 :
256- pkey = b'\04 ' + pkey
275+ sig = sign (claims , self .private_key )
276+ pkey = self .public_key .public_numbers ().encode_point ()
257277 return {
258278 "Authorization" : "{schema} t={t},k={k}" .format (
259279 schema = self ._schema ,
0 commit comments