├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── README.rst ├── js ├── README.md ├── common.js ├── der_lite.js ├── index.html ├── style.css └── vapid.js ├── python ├── .coveragerc ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── claims.json ├── py_vapid │ ├── __init__.py │ ├── jwt.py │ ├── main.py │ ├── tests │ │ └── test_vapid.py │ └── utils.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── test-requirements.txt └── upload.sh └── rust └── vapid ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE └── src ├── error.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | dist/ 4 | include/ 5 | lib/ 6 | local/ 7 | man/ 8 | *.egg-info 9 | *.pem 10 | *.pyc 11 | .*.sw? 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy VAPID generation 2 | 3 | A set of VAPID encoding libraries for popular languages. 4 | 5 | ***PLEASE FEEL FREE TO SUBMIT YOUR FAVORITE LANGUAGE!*** 6 | 7 | VAPID is a draft specification for providing self identification. 8 | see https://datatracker.ietf.org/doc/draft-ietf-webpush-vapid/ 9 | for the latest specification. 10 | 11 | ## TL;DR: 12 | 13 | In short, you create a JSON blob that contains some contact 14 | information about your WebPush feed, for instance: 15 | 16 | ``` 17 | { 18 | "aud": "https://YourSiteHere.example", 19 | "sub": "mailto://admin@YourSiteHere.example", 20 | "exp": 1457718878 21 | } 22 | ``` 23 | 24 | You then convert that to a [JWT](https://tools.ietf.org/html/rfc7519) encoded 25 | with`alg = "ES256"`. The resulting token is the `Authorization` header 26 | "Bearer ..." token, the Public Key used to sign the JWT is added to 27 | the `Crypto-Key` set as "p256ecdsa=..." 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Easy VAPID generation 2 | ===================== 3 | 4 | A set of VAPID encoding libraries for popular languages. 5 | 6 | **PLEASE FEEL FREE TO SUBMIT YOUR FAVORITE LANGUAGE!** 7 | 8 | VAPID is a draft specification for providing self identification. see 9 | https://datatracker.ietf.org/doc/draft-ietf-webpush-vapid/ for the 10 | latest specification. 11 | 12 | TL;DR: 13 | ------ 14 | 15 | In short, you create a JSON blob that contains some contact information 16 | about your WebPush feed, for instance: 17 | 18 | :: 19 | 20 | { 21 | "aud": "https://YourSiteHere.example", 22 | "sub": "mailto://admin@YourSiteHere.example", 23 | "exp": 1457718878 24 | } 25 | 26 | You then convert that to a `JWT `__ 27 | encoded with\ ``alg = "ES256"``. The resulting token is the 28 | ``Authorization`` header “Bearer …” token, the Public Key used to sign 29 | the JWT is added to the ``Crypto-Key`` set as “p256ecdsa=…” 30 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Javascript VAPID library 2 | 3 | This minimal library contains a set of function you need to generate 4 | and verify VAPID headers. 5 | 6 | The index.html file contains a stand-alone generator/verifier. 7 | 8 | [live demo](https://web-push-libs.github.io/vapid/js/) 9 | -------------------------------------------------------------------------------- /js/common.js: -------------------------------------------------------------------------------- 1 | class MozCommon { 2 | 3 | constructor() { 4 | } 5 | 6 | ord(c){ 7 | /* return an ordinal for a character 8 | */ 9 | return c.charCodeAt(0); 10 | } 11 | 12 | chr(c){ 13 | /* return a character for a given ordinal 14 | */ 15 | return String.fromCharCode(c); 16 | } 17 | 18 | toUrlBase64(data) { 19 | /* Convert a binary array into a URL safe base64 string 20 | */ 21 | return btoa(data) 22 | .replace(/\+/g, "-") 23 | .replace(/\//g, "_") 24 | .replace(/=/g, "") 25 | } 26 | 27 | fromUrlBase64(data) { 28 | /* return a binary array from a URL safe base64 string 29 | */ 30 | return atob(data 31 | .replace(/\-/g, "+") 32 | .replace(/\_/g, "/")); 33 | } 34 | 35 | strToArray(str) { 36 | /* convert a string into a ByteArray 37 | * 38 | * TextEncoders would be faster, but have a habit of altering 39 | * byte order 40 | */ 41 | let split = str.split(""); 42 | let reply = new Uint8Array(split.length); 43 | for (let i in split) { 44 | reply[i] = this.ord(split[i]); 45 | } 46 | return reply; 47 | } 48 | 49 | arrayToStr(array) { 50 | /* convert a ByteArray into a string 51 | */ 52 | return String.fromCharCode.apply(null, new Uint8Array(array)); 53 | } 54 | 55 | rawToJWK(raw, ops) { 56 | /* convert a URL safe base64 raw key to jwk format 57 | */ 58 | if (typeof(raw) == "string") { 59 | raw = this.strToArray(this.fromUrlBase64(raw)); 60 | } 61 | // Raw is supposed to start with a 0x04, but some libraries don't. sigh. 62 | if (raw.length == 65 && raw[0] != 4) { 63 | throw new Error('ERR_PUB_KEY'); 64 | } 65 | 66 | raw = raw.slice(-64); 67 | let x = this.toUrlBase64(this.arrayToStr(raw.slice(0,32))); 68 | let y = this.toUrlBase64(this.arrayToSTr(raw.slice(32,64))); 69 | 70 | // Convert to a JWK and import it. 71 | let jwk = { 72 | crv: "P-256", 73 | ext: true, 74 | key_ops: ops, 75 | kty: "EC", 76 | x: x, 77 | y, y 78 | }; 79 | 80 | return jwk 81 | } 82 | 83 | JWKToRaw(jwk) { 84 | /* Convert a JWK object to a "raw" URL Safe base64 string 85 | */ 86 | let xv = this.fromUrlBase64(jwk.x); 87 | let yv = this.fromUrlBase64(jwk.y); 88 | return this.toUrlBase64("\x04" + xv + yv); 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /js/der_lite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class DERLite{ 4 | constructor(mzcc) { 5 | if (mzcc === undefined) { 6 | mzcc = new MozCommon(); 7 | } 8 | this.mzcc = mzcc 9 | } 10 | 11 | /* Simplified DER export and import is provided because a large number of 12 | * libraries and languages understand DER as a key exchange and storage 13 | * format. DER is NOT required for VAPID, however, the key you may 14 | * generate here (or in a different library) may be in this format. 15 | * 16 | * A fully featured DER library is available at 17 | * https://github.com/indutny/asn1.js 18 | */ 19 | 20 | export_private_der(key) { 21 | /* Generate a DER sequence. 22 | * 23 | * This can be read in via something like 24 | * python's 25 | * ecdsa.keys.SigningKey 26 | * .from_der(base64.urlsafe_b64decode("MHc...")) 27 | * 28 | * :param key: CryptoKey containing private key info 29 | */ 30 | return webCrypto.exportKey("jwk", key) 31 | .then(k => { 32 | // verifying key 33 | let xv = this.mzcc.fromUrlBase64(k.x); 34 | let yv = this.mzcc.fromUrlBase64(k.y); 35 | // private key 36 | let dv = this.mzcc.fromUrlBase64(k.d); 37 | 38 | // verifying key (public) 39 | let vk = '\x00\x04' + xv + yv; 40 | // \x02 is integer 41 | let int1 = '\x02\x01\x01'; // integer 1 42 | // \x04 is octet string 43 | let dvstr = '\x04' + this.mzcc.chr(dv.length) + dv; 44 | let curve_oid = "\x06\x08" + 45 | "\x2a\x86\x48\xce\x3d\x03\x01\x07"; 46 | // \xaX is a construct, low byte is order. 47 | let curve_oid_const = '\xa0' + this.mzcc.chr(curve_oid.length) + 48 | curve_oid; 49 | // \x03 is a bitstring 50 | let vk_enc = '\x03' + this.mzcc.chr(vk.length) + vk; 51 | let vk_const = '\xa1' + this.mzcc.chr(vk_enc.length) + vk_enc; 52 | // \x30 is a sequence start. 53 | let seq = int1 + dvstr + curve_oid_const + vk_const; 54 | let rder = "\x30" + this.mzcc.chr(seq.length) + seq; 55 | return this.mzcc.toUrlBase64(rder); 56 | }) 57 | .catch(err => console.error(err)) 58 | } 59 | 60 | import_private_der(der_str) { 61 | /* Import a Private Key stored in DER format. This allows a key 62 | * to be generated outside of this script. 63 | * 64 | * :param der_str: URL safe base64 formatted DER string. 65 | * :returns: Promise containing the imported private key 66 | */ 67 | let der = this.mzcc.strToArray(this.mzcc.fromUrlBase64(der_str)); 68 | // quick guantlet to see if this is a valid DER 69 | let cmp = new Uint8Array([2,1,1,4]); 70 | if (der[0] != 48 || 71 | ! der.slice(2, 6).every(function(v, i){return cmp[i] == v})){ 72 | throw new Error("Invalid import key") 73 | } 74 | let dv = der.slice(7, 7+der[6]); 75 | // HUGE cheat to get the x y values 76 | let xv = der.slice(-64, -32); 77 | let yv = der.slice(-32); 78 | let key_ops = ['sign']; 79 | 80 | let jwk = { 81 | crv: "P-256", 82 | ext: true, 83 | key_ops: key_ops, 84 | kty: "EC", 85 | x: this.mzcc.toUrlBase64(String.fromCharCode.apply(null, xv)), 86 | y: this.mzcc.toUrlBase64(String.fromCharCode.apply(null, yv)), 87 | d: this.mzcc.toUrlBase64(String.fromCharCode.apply(null, dv)), 88 | }; 89 | 90 | console.debug(JSON.stringify(jwk)); 91 | return webCrypto.importKey('jwk', jwk, {name:'ECDSA', namedCurve: 'P-256'} , true, key_ops); 92 | } 93 | 94 | export_public_der(key) { 95 | /* Generate a DER sequence containing just the public key info. 96 | * 97 | * :param key: CryptoKey containing public key information 98 | * :returns: a URL safe base64 encoded string containing the 99 | * public key 100 | */ 101 | return webCrypto.exportKey("jwk", key) 102 | .then(k => { 103 | // raw keys always begin with a 4 104 | let xv = this.mzcc.strToArray(this.mzcc.fromUrlBase64(k.x)); 105 | let yv = this.mzcc.strToArray(this.mzcc.fromUrlBase64(k.y)); 106 | 107 | let point = "\x00\x04" + 108 | String.fromCharCode.apply(null, xv) + 109 | String.fromCharCode.apply(null, yv); 110 | window.Kpoint = point; 111 | // a combination of the oid_ecPublicKey + p256 encoded oid 112 | let prefix = "\x30\x13" + // sequence + length 113 | "\x06\x07" + "\x2a\x86\x48\xce\x3d\x02\x01" + 114 | "\x06\x08" + "\x2a\x86\x48\xce\x3d\x03\x01\x07" 115 | let encPoint = "\x03" + this.mzcc.chr(point.length) + point 116 | let rder = "\x30" + this.mzcc.chr(prefix.length + encPoint.length) + 117 | prefix + encPoint; 118 | let der = this.mzcc.toUrlBase64(rder); 119 | return der; 120 | }); 121 | } 122 | 123 | import_public_der(derArray) { 124 | /* Import a DER formatted public key string. 125 | * 126 | * The Crypto-Key p256ecdsa=... key is such a thing. 127 | * Returns a promise containing the public key. 128 | * 129 | * :param derArray: the DER array containing the public key. 130 | * NOTE: This may also be a URL safe base64 encoded version 131 | * of the DER array. 132 | * :returns: A promise containing the imported public key. 133 | * 134 | */ 135 | if (typeof(derArray) == "string") { 136 | derArray = this.mzcc.strToArray(this.mzcc.fromUrlBase64(derArray)); 137 | } 138 | /* Super light weight public key import function */ 139 | let err = new Error(this.lang.errs.ERR_PUB_D_KEY); 140 | // Does the record begin with "\x30" 141 | if (derArray[0] != 48) { throw err} 142 | // is this an ECDSA record? (looking for \x2a and \x86 143 | if (derArray[6] != 42 && derArray[7] != 134) { throw err} 144 | if (derArray[15] != 42 && derArray[16] != 134) { throw err} 145 | // Public Key Record usually beings @ offset 23. 146 | if (derArray[23] != 3 && derArray[24] != 40 && 147 | derArray[25] != 0 && derArray[26] != 4) { 148 | throw err; 149 | } 150 | // pubkey offset starts at byte 25 151 | let x = this.mzcc.toUrlBase64(String.fromCharCode 152 | .apply(null, derArray.slice(27, 27+32))); 153 | let y = this.mzcc.toUrlBase64(String.fromCharCode 154 | .apply(null, derArray.slice(27+32, 27+64))); 155 | 156 | // Convert to a JWK and import it. 157 | let jwk = { 158 | crv: "P-256", 159 | ext: true, 160 | key_ops: ["verify"], 161 | kty: "EC", 162 | x: x, 163 | y, y 164 | }; 165 | 166 | return webCrypto.importKey('jwk', jwk, {name:'ECDSA', namedCurve: 'P-256'}, true, ["verify"]) 167 | 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VAPID verification page 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |

VAPID verification

13 |
14 |

This page helps validate or construct 15 | VAPID 16 | header data. The Headers section accepts existing header values 17 | (e.g. ones created by a library or included as part of a subscription 18 | update).

19 |

The Claims section will create a valid JSON claim, generate a 20 | VAPID key pair, and generate the proper header values.

21 |

The Exported Keys section provides the keys in DER format which 22 | should be readable by many encryption libraries.

23 |
24 |
25 |
26 | 27 | 28 |
29 |

Headers

30 |

The headers are sent with subscription updates. They provide the 31 | site information to associate with this feed. PLEASE NOTE: Your private 32 | key should be generated on your machine and should NEVER leave your 33 | box or control. This page will generate a valid key that can be used, 34 | and all functions are local, but this is purely for educational purposes 35 | only.

36 | 37 |
This is the content of the 38 | Authorization header included as part of the subscription 39 | POST update.
40 | 41 |
42 | 43 |
This is included 44 | as part of the Crypto-Key header, which is included 45 | as part of the subscription POST update. Crypto-Key 46 | may contain more than one part. Each part should be separated by a 47 | comma (",") (NOTE: For Draft-02, this header is not required)
48 | 49 |
50 | 51 |
52 |
53 |
54 |

Claims

55 |

Claims are the information a site uses to identify itself. 56 |

57 | 58 |

The required administrative email address that can be contacted if there's an issue

59 | 60 |
61 |
62 | 63 |

Time in seconds for this claim to live. (Default/Max: 24 hours from now)

64 | 65 |
66 |

 

67 |

Note: You can add more claims if you wish. 68 | These can include things 69 | like, the ID of the originating server (if you have several that may be 70 | publishing updates), a proxied customer ID or hash (for privacy reasons, 71 | you probably don't want to make this easily determinable), or any other 72 | value that may be useful between the Push Server Ops team and yours. 73 | Just make the values short so you don't run the risk of the server 74 | rejecting a request because the headers are too big.

75 |

For example: 76 | {'sub': 'push@example.org', 'exp': 1470858133, 77 | 'ami_id':'e-1248296','cust_id':'a9afd519s919faio3'}

78 |
79 | 80 |
81 |

Claims JSON object:

82 |
None
83 |
84 |
85 |

Exported Keys

86 | Auto-generated keys: 87 |

These are ASN.1 DER formatted version of the public and private keys used 88 | to generate the above VAPID headers. These can be useful for languages that 89 | use DER or PEM for key import.

90 | 91 | 92 |
93 | 94 |
95 | 96 |
97 | 98 | 99 | 100 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /js/style.css: -------------------------------------------------------------------------------- 1 | html {font-family:Arial;} 2 | .section {position:relative; padding: .5em; 3 | width:80%; margin:0 auto;} 4 | label{margin-top:.5em;} 5 | button{border:1px solid black; border-radius: 5pt; margin:5px; font-weight:bold} 6 | .section label {display:block;position:relative;right:0;} 7 | .section textarea, .section .value { 8 | width:95%;white-space:pre-wrap; 9 | position:relative; left:3%; 10 | min-height:4em; 11 | word-break:break-all;vertical-align:top;} 12 | .value {color: #666;} 13 | .description{margin: 0 3%;font-size:90%;} 14 | .hidden {display:none;} 15 | .section label {width:13em;} 16 | #err {color:red;} 17 | .err {color:red; border:1px solid red;} 18 | #raw_claims{white-space:pre-wrap;} 19 | input, .value { 20 | transition: background-color; 21 | border:1px solid #ccc; 22 | } 23 | @keyframes update { 24 | from{background-color: auto;} 25 | to { background-color: #ffd699;} 26 | } 27 | .updated { 28 | background-color: auto; 29 | animation-duration: 1s; 30 | animation-name: update; 31 | animation-iteration-count: 2; 32 | animation-direction: alternate; 33 | } 34 | p{margin:0 3%; font-size:90%;} 35 | #result input {margin-left:3%; width:20em;} 36 | .good::after {content:" ✓"; font-weight:bold;color:green;} 37 | .bad::after {content:" !"; font-weight:bold;color:red;} 38 | -------------------------------------------------------------------------------- /js/vapid.js: -------------------------------------------------------------------------------- 1 | /* Javascript VAPID library. 2 | * 3 | * Requires: common.js 4 | * 5 | */ 6 | 7 | 'use strict'; 8 | 9 | try { 10 | if (webCrypto === undefined) { 11 | webCrypto = window.crypto.subtle; 12 | } 13 | } catch (e) { 14 | var webCrypto = window.crypto.subtle; 15 | } 16 | 17 | class VapidCore { 18 | constructor(aud, sub, exp, lang, mzcc) { 19 | /* Construct a base VAPID token. 20 | * 21 | * VAPID allows for self identification of a subscription update. 22 | * 23 | * :param sub: Subscription - email of the admin contact for this 24 | * update. 25 | * :param exp: Expiration - UTC expiration of this update. Defaults 26 | * to now + 24 hours 27 | */ 28 | 29 | if (mzcc == undefined) { 30 | mzcc = new MozCommon(); 31 | } 32 | this.mzcc = mzcc; 33 | this._claims={}; 34 | this._claims['aud'] = aud || ""; 35 | if (sub !== undefined) { 36 | this._claims['sub'] = sub; 37 | } 38 | if (exp == undefined) { 39 | // Set expry to be 24 hours from now. 40 | exp = (Date.now() * .001) + 86400 41 | } 42 | this._claims["exp"] = exp 43 | let enus = { 44 | info: { 45 | OK_VAPID_KEYS: "VAPID Keys defined.", 46 | }, 47 | errs: { 48 | ERR_VAPID_KEY: "VAPID generate keys error: ", 49 | ERR_PUB_R_KEY: "Invalid Public Key record. Please use a valid RAW Formatted record.", 50 | ERR_PUB_D_KEY: "Invalid Public Key record. Please use a valid DER Formatted record.", 51 | ERR_NO_KEYS: "No keys defined. Please use generate_keys() or load a public key.", 52 | ERR_CLAIM_MIS: "Claim missing ", 53 | ERR_SIGN: "Sign error", 54 | ERR_VERIFY_SG: "Verify Error: Auth signature invalid: ", 55 | ERR_VERIFY_KE: "Verify Error: Key invalid: ", 56 | ERR_SIGNATURE: "Signature Invalid", 57 | ERR_VERIFY: "Verify error", 58 | } 59 | }; 60 | this.lang = enus; 61 | 62 | this._private_key = ""; 63 | this._public_key = ""; 64 | 65 | } 66 | 67 | generate_keys() { 68 | /* Generate the public and private keys 69 | */ 70 | return webCrypto.generateKey( 71 | {name: "ECDSA", namedCurve: "P-256"}, 72 | true, 73 | ["sign", "verify"]) 74 | .then(keys => { 75 | this._private_key = keys.privateKey; 76 | this._public_key = keys.publicKey; 77 | console.info(this.lang.info.OK_VAPID_KEYS); 78 | return keys; 79 | }) 80 | .catch(fail => { 81 | console.error(this.lang.errs.ERR_VAPID_KEY, fail); 82 | throw(fail); 83 | }); 84 | } 85 | 86 | export_public_raw() { 87 | /* Export an ASN1 RAW key pair. 88 | * 89 | * This is used in the Crypto-Key header. 90 | * 91 | * NOTE: Chrome 52 does not yet support RAW keys 92 | */ 93 | return webCrypto.exportKey('jwk', this._public_key) 94 | .then( key => { 95 | return this.mzcc.toUrlBase64("\x04" + 96 | this.mzcc.fromUrlBase64(key.x) + 97 | this.mzcc.fromUrlBase64(key.y)) 98 | }) 99 | .catch(err => { 100 | console.error("public raw format", err); 101 | throw err; 102 | }) 103 | } 104 | 105 | import_public_raw(raw) { 106 | /* Import an ASN1 RAW public key pair. 107 | * 108 | * :param raw: a URL safe base64 encoded rendition of the RAW key. 109 | * :returns: a promise from the imported key. 110 | */ 111 | if (typeof(raw) == "string") { 112 | raw = this.mzcc.strToArray(this.mzcc.fromUrlBase64(raw)); 113 | } 114 | let err = new Error(this.lang.errs.ERR_PUB_KEY); 115 | 116 | // Raw is supposed to start with a 0x04, but some libraries don't. sigh. 117 | if (raw.length == 65 && raw[0] != 4) { 118 | throw err; 119 | } 120 | 121 | raw= raw.slice(-64); 122 | let x = this.mzcc.toUrlBase64(String.fromCharCode.apply(null, 123 | raw.slice(0,32))); 124 | let y = this.mzcc.toUrlBase64(String.fromCharCode.apply(null, 125 | raw.slice(32,64))); 126 | 127 | // Convert to a JWK and import it. 128 | let jwk = { 129 | crv: "P-256", 130 | ext: true, 131 | key_ops: ["verify"], 132 | kty: "EC", 133 | x: x, 134 | y: y, 135 | }; 136 | 137 | return webCrypto.importKey('jwk', jwk, 'ECDSA', true, ["verify"]) 138 | .catch(err => { 139 | if (err instanceof TypeError && /namedCurve/.test(err.stack)) 140 | return webCrypto.importKey('jwk', jwk, { 141 | name: "ECDSA", 142 | namedCurve: "P-256" 143 | }, true, ["verify"]) 144 | .then(k => this._public_key = k) 145 | throw err 146 | }) 147 | .then(k => this._public_key = k) 148 | } 149 | 150 | _sign(claims) { 151 | /* Sign a claims object and return the headers that can be used to 152 | * decrypt the string. 153 | * 154 | * :param claims: An object containing the VAPID claims. 155 | * :returns: a promise containing an object identifying the headers 156 | * and values to include to specify VAPID auth. 157 | */ 158 | if (! claims) { 159 | claims = this._claims; 160 | } 161 | if (this._public_key == "") { 162 | throw new Error(this.lang.errs.ERR_NO_KEYS); 163 | } 164 | if (! claims.hasOwnProperty("exp")) { 165 | claims.exp = parseInt(Date.now()*.001) + 86400; 166 | } 167 | if (! claims.hasOwnProperty("sub")) { 168 | throw new Error(this.lang.errs.ERR_CLAIM_MIS, "sub"); 169 | } 170 | let alg = {name:"ECDSA", namedCurve: "P-256", hash:{name:"SHA-256"}}; 171 | let headStr = this.mzcc.toUrlBase64( 172 | JSON.stringify({typ:"JWT",alg:"ES256"})); 173 | let claimStr = this.mzcc.toUrlBase64( 174 | JSON.stringify(claims)); 175 | let content = headStr + "." + claimStr; 176 | let signatory = this.mzcc.strToArray(content); 177 | return webCrypto.sign( 178 | alg, 179 | this._private_key, 180 | signatory) 181 | .then(signature => { 182 | let sig = this.mzcc.toUrlBase64( 183 | this.mzcc.arrayToStr(signature)); 184 | /* The headers consist of the constructed JWT as the 185 | * "authorization" and the raw Public key as the p256ecdsa 186 | * element of "Crypto-Key" 187 | * Note that Crypto-Key can contain many elements, separated 188 | * by a ",".i You may need to append this value to an existing 189 | * "Crypto-Key" header value. 190 | * 191 | * 192 | */ 193 | return this.export_public_raw() 194 | .then( pubKey => { 195 | return { 196 | jwt: content + "." + sig, 197 | pubkey: pubKey, 198 | } 199 | }) 200 | }) 201 | .catch(err => { 202 | console.error(this.lang.errs.ERR_SIGN, err); 203 | }) 204 | } 205 | 206 | _verify(token) { 207 | /* Verify a VAPID token. 208 | * 209 | * Token is the Authorization Header, Public Key is the Crypto-Key 210 | * header. 211 | * 212 | * :param token: the Authorization header bearer token 213 | */ 214 | 215 | // Ideally, just the bearer token, Cheat a little to be nice to the dev. 216 | if (this._public_key == "") { 217 | throw new Error(this.lang.errs.ERR_NO_KEYS); 218 | } 219 | 220 | let alg = {name: "ECDSA", namedCurve: "P-256", 221 | hash: {name: "SHA-256" }}; 222 | let items = token.split('.'); 223 | let signature; 224 | let key; 225 | try { 226 | signature = this.mzcc.strToArray( 227 | this.mzcc.fromUrlBase64(items[2])); 228 | } catch (err) { 229 | throw new Error(this.lang.errs.ERR_VERIFY_SG + err.message); 230 | } 231 | try { 232 | key = this.mzcc.strToArray(this.mzcc.fromUrlBase64(items[1])); 233 | } catch (err) { 234 | throw new Error(this.lang.errs.ERR_VERIFY_KE + err.message); 235 | } 236 | let content = items.slice(0,2).join('.'); 237 | let signatory = this.mzcc.strToArray(content); 238 | return webCrypto.verify( 239 | alg, 240 | this._public_key, 241 | signature, 242 | signatory) 243 | .then(valid => { 244 | if (valid) { 245 | return JSON.parse( 246 | String.fromCharCode.apply( 247 | null, 248 | this.mzcc.strToArray( 249 | this.mzcc.fromUrlBase64(items[1])))) 250 | } 251 | throw new Error(this.lang.errs.ERR_SIGNATURE); 252 | }) 253 | .catch(err => { 254 | console.error(this.lang.errs.ERR_VERIFY, err); 255 | throw new Error (this.lang.errs.ERR_VERIFY + ": " + err.message); 256 | }); 257 | } 258 | 259 | /* The following are for the Dashboard key ownership validation steps. 260 | * The Mozilla WebPush dashboard will provide a token, which you will 261 | * need to sign with your Vapid Private Key. Paste the signature back 262 | * into the dashboard to validate that you own the key. 263 | */ 264 | 265 | validate(string) { 266 | /* Sign the token for the developer Dashboard. 267 | * 268 | * The Developer Dashboard requires that a token be signed using 269 | * the VAPID private key in order to show that a user actually 270 | * owns their public key. 271 | * 272 | * :param string: The token provided by the Dashboard Validate 273 | * function 274 | * :returns: the signature value to paste back into the Dashboard. 275 | */ 276 | let alg = {name:"ECDSA", namedCurve: "P-256", hash:{name:"SHA-256"}}; 277 | let t2v = this.mzcc.strToArray(string); 278 | return webCrypto.sign(alg, this._private_key, t2v) 279 | .then(signed => { 280 | let sig = this.mzcc.toUrlBase64(this.mzcc.arrayToStr(signed)); 281 | return sig; 282 | }); 283 | } 284 | 285 | validateCheck(sig, string) { 286 | /* verify a given signature string matches. 287 | * 288 | * This function is used for testing only. 289 | * 290 | * :param sig: The signature value generated by validate() 291 | * :param string: The token string originally passed to validate 292 | * :returns: Boolean indicating successful verification. 293 | */ 294 | let alg = {name: "ECDSA", namedCurve: "P-256", hash:{name:"SHA-256"}}; 295 | let vsig = this.mzcc.strToArray(this.mzcc.fromUrlBase64(sig)); 296 | let t2v = this.mzcc.strToArray(this.mzcc.fromUrlBase64(string)); 297 | return webCrypto.verify(alg, this._public_key, vsig, t2v); 298 | } 299 | } 300 | 301 | class VapidToken01 extends VapidCore { 302 | 303 | sign(claims) { 304 | return this._sign(claims) 305 | .then(elements=> { 306 | return { 307 | authorization: "WebPush " + elements.jwt, 308 | "crypto-key": "p256ecdsa=" + elements.pubkey, 309 | publicKey: elements.pubkey, 310 | } 311 | } 312 | ) 313 | } 314 | 315 | verify(token, public_key) { 316 | let scheme = token.toLowerCase().split(" ")[0] 317 | if (scheme == "bearer" || scheme == "webpush") { 318 | token = token.split(" ")[1]; 319 | } 320 | 321 | // Again, ideally, just the p256ecdsa token. 322 | if (public_key != null) { 323 | 324 | if (public_key.search('p256ecdsa') > -1) { 325 | let sc = /p256ecdsa=([^;,]+)/i; 326 | public_key = sc.exec(public_key)[1]; 327 | } 328 | 329 | // If there's no public key already defined, load the public_key 330 | // and try again. 331 | return this.import_public_raw(public_key) 332 | .then(key => { 333 | this._public_key = key; 334 | return this._verify(token); 335 | }) 336 | .catch(err => { 337 | console.error("Verify error", err); 338 | throw err; 339 | }); 340 | } 341 | 342 | return this._verify(token) 343 | } 344 | } 345 | 346 | class VapidToken02 extends VapidCore { 347 | 348 | sign(claims) { 349 | return this._sign(claims) 350 | .then(elements=> { 351 | return { 352 | authorization: "vapid t=" + elements.jwt + ",k=" + elements.pubkey, 353 | publicKey: elements.pubkey, 354 | } 355 | } 356 | ) 357 | } 358 | 359 | verify(token) { 360 | let scheme = token.toLowerCase().split(" ")[0] 361 | if (scheme == "vapid") { 362 | token = token.split(" ")[1]; 363 | } 364 | let vals = {}; 365 | let elements = token.split(","); 366 | for (let element of elements) { 367 | let label = element.slice(0,2); 368 | if (label == "t=") { 369 | vals.t = element.slice(2); 370 | } 371 | if (label == "k=") { 372 | vals.k = element.slice(2); 373 | } 374 | } 375 | return this.import_public_raw(vals.k) 376 | .then(key => { 377 | this._public_key = key; 378 | return this._verify(vals.t); 379 | }) 380 | .catch(err => { 381 | console.error("Verify error", err); 382 | throw err; 383 | }); 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /python/.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = *noseplugin* 3 | show_missing = true 4 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | .eggs/ 2 | -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /python/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.txt 3 | include setup.* 4 | include LICENSE 5 | recursive-include py_vapid *.py 6 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Easy VAPID generation 2 | 3 | [![PyPI version py_vapid](https://badge.fury.io/py/py-vapid.svg)](https://pypi.org/project/py-vapid/) 4 | 5 | This library is available on [pypi as py-vapid](https://pypi.python.org/pypi/py-vapid). 6 | Source is available on [github](https://github.com/mozilla-services/vapid). 7 | Please note: This library was designated as a `Critical Project` by PyPi, it is currently 8 | maintained by [a single person](https://xkcd.com/2347/). I still accept PRs and Issues, but 9 | make of that what you will. 10 | 11 | This minimal library contains the minimal set of functions you need to 12 | generate a VAPID key set and get the headers you'll need to sign a 13 | WebPush subscription update. 14 | 15 | VAPID is a voluntary standard for WebPush subscription providers 16 | (sites that send WebPush updates to remote customers) to self-identify 17 | to Push Servers (the servers that convey the push notifications). 18 | 19 | The VAPID "claims" are a set of JSON keys and values. There are two 20 | required fields, one semi-optional and several optional additional 21 | fields. 22 | 23 | At a minimum a VAPID claim set should look like: 24 | 25 | ```json 26 | {"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServer","exp":"ExpirationTimestamp"} 27 | ``` 28 | 29 | A few notes: 30 | 31 | ***sub*** is the email address you wish to have on record for this 32 | request, prefixed with "`mailto:`". If things go wrong, this is the 33 | email that will be used to contact you (for instance). This can be a 34 | general delivery address like "`mailto:push_operations@example.com`" or a 35 | specific address like "`mailto:bob@example.com`". 36 | 37 | ***aud*** is the audience for the VAPID. This is the scheme and host 38 | you use to send subscription endpoints and generally coincides with 39 | the `endpoint` specified in the Subscription Info block. 40 | 41 | As example, if a WebPush subscription info contains: 42 | `{"endpoint": "https://push.example.com:8012/v1/push/...", ...}` 43 | 44 | then the `aud` would be "`https://push.example.com:8012`" 45 | 46 | While some Push Services consider this an optional field, others may 47 | be stricter. 48 | 49 | ***exp*** This is the UTC timestamp for when this VAPID request will 50 | expire. The maximum period is 24 hours. Setting a shorter period can 51 | prevent "replay" attacks. Setting a longer period allows you to reuse 52 | headers for multiple sends (e.g. if you're sending hundreds of updates 53 | within an hour or so.) If no `exp` is included, one that will expire 54 | in 24 hours will be auto-generated for you. 55 | 56 | Claims should be stored in a JSON compatible file. In the examples 57 | below, we've stored the claims into a file named `claims.json`. 58 | 59 | py_vapid can either be installed as a library or used as a stand along 60 | app, `bin/vapid`. 61 | 62 | ## App Installation 63 | 64 | You'll need `python virtualenv` Run that in the current directory. 65 | 66 | Then run 67 | 68 | ```python 69 | bin/pip install -r requirements.txt 70 | 71 | bin/python -m pip install -e . 72 | ``` 73 | 74 | ## App Usage 75 | 76 | Run by itself, `bin/vapid` will check and optionally create the 77 | public_key.pem and private_key.pem files. 78 | 79 | `bin/vapid --gen` can be used to generate a new set of public and 80 | private key PEM files. These will overwrite the contents of 81 | `private_key.pem` and `public_key.pem`. 82 | 83 | `bin/vapid --sign claims.json` will generate a set of HTTP headers 84 | from a JSON formatted claims file. A sample `claims.json` is included 85 | with this distribution. 86 | 87 | `bin/vapid --sign claims.json --json` will output the headers in 88 | JSON format, which may be useful for other programs. 89 | 90 | `bin/vapid --applicationServerKey` will return the 91 | `applicationServerKey` value you can use to make a restricted 92 | endpoint. See 93 | https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe 94 | for more details. Be aware that this value is tied to the generated 95 | public/private key. If you remove or generate a new key, any 96 | restricted URL you've previously generated will need to be 97 | reallocated. Please note that some User Agents may require you [to 98 | decode this string into a Uint8Array](https://github.com/GoogleChrome/push-notifications/blob/master/app/scripts/main.js). 99 | 100 | See `bin/vapid -h` for all options and commands. 101 | 102 | ## CHANGELOG 103 | 104 | I'm terrible about updating the Changelog. Please see the [`git 105 | log`](https://github.com/web-push-libs/vapid/pulls?q=is%3Apr+is%3Aclosed) 106 | history for details. 107 | -------------------------------------------------------------------------------- /python/README.rst: -------------------------------------------------------------------------------- 1 | |PyPI version py_vapid| 2 | 3 | Easy VAPID generation 4 | ===================== 5 | 6 | This minimal library contains the minimal set of functions you need to 7 | generate a VAPID key set and get the headers you’ll need to sign a 8 | WebPush subscription update. 9 | 10 | VAPID is a voluntary standard for WebPush subscription providers (sites 11 | that send WebPush updates to remote customers) to self-identify to Push 12 | Servers (the servers that convey the push notifications). 13 | 14 | The VAPID “claims” are a set of JSON keys and values. There are two 15 | required fields, one semi-optional and several optional additional 16 | fields. 17 | 18 | At a minimum a VAPID claim set should look like: 19 | 20 | :: 21 | 22 | {"sub":"mailto:YourEmail@YourSite.com","aud":"https://PushServer","exp":"ExpirationTimestamp"} 23 | 24 | A few notes: 25 | 26 | **sub** is the email address you wish to have on record for this 27 | request, prefixed with “``mailto:``”. If things go wrong, this is the 28 | email that will be used to contact you (for instance). This can be a 29 | general delivery address like “``mailto:push_operations@example.com``” 30 | or a specific address like “``mailto:bob@example.com``”. 31 | 32 | **aud** is the audience for the VAPID. This is the scheme and host you 33 | use to send subscription endpoints and generally coincides with the 34 | ``endpoint`` specified in the Subscription Info block. 35 | 36 | As example, if a WebPush subscription info contains: 37 | ``{"endpoint": "https://push.example.com:8012/v1/push/...", ...}`` 38 | 39 | then the ``aud`` would be “``https://push.example.com:8012``” 40 | 41 | While some Push Services consider this an optional field, others may be 42 | stricter. 43 | 44 | **exp** This is the UTC timestamp for when this VAPID request will 45 | expire. The maximum period is 24 hours. Setting a shorter period can 46 | prevent “replay” attacks. Setting a longer period allows you to reuse 47 | headers for multiple sends (e.g. if you’re sending hundreds of updates 48 | within an hour or so.) If no ``exp`` is included, one that will expire 49 | in 24 hours will be auto-generated for you. 50 | 51 | Claims should be stored in a JSON compatible file. In the examples 52 | below, we’ve stored the claims into a file named ``claims.json``. 53 | 54 | py_vapid can either be installed as a library or used as a stand along 55 | app, ``bin/vapid``. 56 | 57 | App Installation 58 | ---------------- 59 | 60 | You’ll need ``python virtualenv`` Run that in the current directory. 61 | 62 | Then run 63 | 64 | :: 65 | 66 | bin/pip install -r requirements.txt 67 | 68 | bin/python -m pip install -e . 69 | 70 | App Usage 71 | --------- 72 | 73 | Run by itself, ``bin/vapid`` will check and optionally create the 74 | public_key.pem and private_key.pem files. 75 | 76 | ``bin/vapid --gen`` can be used to generate a new set of public and 77 | private key PEM files. These will overwrite the contents of 78 | ``private_key.pem`` and ``public_key.pem``. 79 | 80 | ``bin/vapid --sign claims.json`` will generate a set of HTTP headers 81 | from a JSON formatted claims file. A sample ``claims.json`` is included 82 | with this distribution. 83 | 84 | ``bin/vapid --sign claims.json --json`` will output the headers in JSON 85 | format, which may be useful for other programs. 86 | 87 | ``bin/vapid --applicationServerKey`` will return the 88 | ``applicationServerKey`` value you can use to make a restricted 89 | endpoint. See 90 | https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe 91 | for more details. Be aware that this value is tied to the generated 92 | public/private key. If you remove or generate a new key, any restricted 93 | URL you’ve previously generated will need to be reallocated. Please note 94 | that some User Agents may require you `to decode this string into a 95 | Uint8Array `__. 96 | 97 | See ``bin/vapid -h`` for all options and commands. 98 | 99 | CHANGELOG 100 | --------- 101 | 102 | I’m terrible about updating the Changelog. Please see the 103 | ```git log`` `__ 104 | history for details. 105 | 106 | .. |PyPI version py_vapid| image:: https://badge.fury.io/py/py-vapid.svg 107 | :target: https://pypi.org/project/py-vapid/ 108 | -------------------------------------------------------------------------------- /python/claims.json: -------------------------------------------------------------------------------- 1 | { 2 | "sub": "mailto:admin@example.com", 3 | "aud": "https://push.services.mozilla.com" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /python/py_vapid/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import logging 7 | import binascii 8 | import time 9 | import re 10 | import copy 11 | 12 | from cryptography.hazmat.backends import default_backend 13 | from cryptography.hazmat.primitives.asymmetric import ec, utils as ecutils 14 | from cryptography.hazmat.primitives import serialization 15 | 16 | from cryptography.hazmat.primitives import hashes 17 | from cryptography.exceptions import InvalidSignature 18 | 19 | from py_vapid.utils import b64urldecode, b64urlencode 20 | from py_vapid.jwt import sign 21 | 22 | # Show compliance version. For earlier versions see previously tagged releases. 23 | VERSION = "VAPID-RFC/ECE-RFC" 24 | 25 | 26 | class VapidException(Exception): 27 | """An exception wrapper for Vapid.""" 28 | pass 29 | 30 | 31 | class Vapid01(object): 32 | """Minimal VAPID Draft 01 signature generation library. 33 | 34 | https://tools.ietf.org/html/draft-ietf-webpush-vapid-01 35 | 36 | """ 37 | _private_key = None 38 | _public_key = None 39 | _schema = "WebPush" 40 | 41 | def __init__(self, private_key=None, conf=None): 42 | """Initialize VAPID with an optional private key. 43 | 44 | :param private_key: A private key object 45 | :type private_key: ec.EllipticCurvePrivateKey 46 | 47 | """ 48 | if conf is None: 49 | conf = {} 50 | self.conf = conf 51 | self.private_key = private_key 52 | if private_key: 53 | self._public_key = self.private_key.public_key() 54 | 55 | @classmethod 56 | def from_raw(cls, private_raw): 57 | """Initialize VAPID using a private key point in "raw" or 58 | "uncompressed" form. Raw keys consist of a single, 32 octet 59 | encoded integer. 60 | 61 | :param private_raw: A private key point in uncompressed form. 62 | :type private_raw: bytes 63 | 64 | """ 65 | key = ec.derive_private_key( 66 | int(binascii.hexlify(b64urldecode(private_raw)), 16), 67 | curve=ec.SECP256R1(), 68 | backend=default_backend()) 69 | return cls(key) 70 | 71 | @classmethod 72 | def from_raw_public(cls, public_raw): 73 | key = ec.EllipticCurvePublicKey.from_encoded_point( 74 | curve=ec.SECP256R1(), 75 | data=b64urldecode(public_raw) 76 | ) 77 | ss = cls() 78 | ss._public_key = key 79 | return ss 80 | 81 | @classmethod 82 | def from_pem(cls, private_key): 83 | """Initialize VAPID using a private key in PEM format. 84 | 85 | :param private_key: A private key in PEM format. 86 | :type private_key: bytes 87 | 88 | """ 89 | # not sure why, but load_pem_private_key fails to deserialize 90 | return cls.from_der( 91 | b''.join(private_key.splitlines()[1:-1])) 92 | 93 | @classmethod 94 | def from_der(cls, private_key): 95 | """Initialize VAPID using a private key in DER format. 96 | 97 | :param private_key: A private key in DER format and Base64-encoded. 98 | :type private_key: bytes 99 | 100 | """ 101 | key = serialization.load_der_private_key(b64urldecode(private_key), 102 | password=None, 103 | backend=default_backend()) 104 | return cls(key) 105 | 106 | @classmethod 107 | def from_file(cls, private_key_file=None): 108 | """Initialize VAPID using a file containing a private key in PEM or 109 | DER format. 110 | 111 | :param private_key_file: Name of the file containing the private key 112 | :type private_key_file: str 113 | 114 | """ 115 | if not os.path.isfile(private_key_file): 116 | logging.info("Private key not found, generating key...") 117 | vapid = cls() 118 | vapid.generate_keys() 119 | vapid.save_key(private_key_file) 120 | return vapid 121 | with open(private_key_file, 'r') as file: 122 | private_key = file.read() 123 | try: 124 | if "-----BEGIN" in private_key: 125 | vapid = cls.from_pem(private_key.encode('utf8')) 126 | else: 127 | vapid = cls.from_der(private_key.encode('utf8')) 128 | return vapid 129 | except Exception as exc: 130 | logging.error("Could not open private key file: %s", repr(exc)) 131 | raise VapidException(exc) 132 | 133 | @classmethod 134 | def from_string(cls, private_key): 135 | """Initialize VAPID using a string containing the private key. This 136 | will try to determine if the key is in RAW or DER format. 137 | 138 | :param private_key: String containing the key info 139 | :type private_key: str 140 | 141 | """ 142 | 143 | pkey = private_key.encode().replace(b"\n", b"") 144 | key = b64urldecode(pkey) 145 | if len(key) == 32: 146 | return cls.from_raw(pkey) 147 | return cls.from_der(pkey) 148 | 149 | @classmethod 150 | def verify(cls, key, auth): 151 | """Verify a VAPID authorization token. 152 | 153 | :param key: base64 serialized public key 154 | :type key: str 155 | :param auth: authorization token 156 | type key: str 157 | 158 | """ 159 | tokens = auth.rsplit(' ', 1)[1].rsplit('.', 1) 160 | kp = cls().from_raw_public(key.encode()) 161 | return kp.verify_token( 162 | validation_token=tokens[0].encode(), 163 | verification_token=tokens[1] 164 | ) 165 | 166 | @property 167 | def private_key(self): 168 | """The VAPID private ECDSA key""" 169 | if not self._private_key: 170 | raise VapidException("No private key. Call generate_keys()") 171 | return self._private_key 172 | 173 | @private_key.setter 174 | def private_key(self, value): 175 | """Set the VAPID private ECDSA key 176 | 177 | :param value: the byte array containing the private ECDSA key data 178 | :type value: ec.EllipticCurvePrivateKey 179 | 180 | """ 181 | self._private_key = value 182 | if value: 183 | self._public_key = self.private_key.public_key() 184 | 185 | @property 186 | def public_key(self): 187 | """The VAPID public ECDSA key 188 | 189 | The public key is currently read only. Set it via the `.private_key` 190 | method. This will autogenerate a public and private key if no value 191 | has been set. 192 | 193 | :returns ec.EllipticCurvePublicKey 194 | 195 | """ 196 | return self._public_key 197 | 198 | def generate_keys(self): 199 | """Generate a valid ECDSA Key Pair.""" 200 | self.private_key = ec.generate_private_key(ec.SECP256R1, 201 | default_backend()) 202 | 203 | def private_pem(self): 204 | return self.private_key.private_bytes( 205 | encoding=serialization.Encoding.PEM, 206 | format=serialization.PrivateFormat.PKCS8, 207 | encryption_algorithm=serialization.NoEncryption() 208 | ) 209 | 210 | def public_pem(self): 211 | return self.public_key.public_bytes( 212 | encoding=serialization.Encoding.PEM, 213 | format=serialization.PublicFormat.SubjectPublicKeyInfo 214 | ) 215 | 216 | def save_key(self, key_file): 217 | """Save the private key to a PEM file. 218 | 219 | :param key_file: The file path to save the private key data 220 | :type key_file: str 221 | 222 | """ 223 | with open(key_file, "wb") as file: 224 | file.write(self.private_pem()) 225 | file.close() 226 | 227 | def save_public_key(self, key_file): 228 | """Save the public key to a PEM file. 229 | :param key_file: The name of the file to save the public key 230 | :type key_file: str 231 | 232 | """ 233 | with open(key_file, "wb") as file: 234 | file.write(self.public_pem()) 235 | file.close() 236 | 237 | def verify_token(self, validation_token, verification_token): 238 | """Internally used to verify the verification token is correct. 239 | 240 | :param validation_token: Provided validation token string 241 | :type validation_token: str 242 | :param verification_token: Generated verification token 243 | :type verification_token: str 244 | :returns: Boolean indicating if verifictation token is valid. 245 | :rtype: boolean 246 | 247 | """ 248 | hsig = b64urldecode(verification_token.encode('utf8')) 249 | r = int(binascii.hexlify(hsig[:32]), 16) 250 | s = int(binascii.hexlify(hsig[32:]), 16) 251 | try: 252 | self.public_key.verify( 253 | ecutils.encode_dss_signature(r, s), 254 | validation_token, 255 | signature_algorithm=ec.ECDSA(hashes.SHA256()) 256 | ) 257 | return True 258 | except InvalidSignature: 259 | return False 260 | 261 | def _base_sign(self, claims): 262 | cclaims = copy.deepcopy(claims) 263 | if not cclaims.get('exp'): 264 | cclaims['exp'] = int(time.time()) + 86400 265 | if not self.conf.get('no-strict', False): 266 | valid = _check_sub(cclaims.get('sub', '')) 267 | else: 268 | valid = cclaims.get('sub') is not None 269 | if not valid: 270 | raise VapidException( 271 | "Missing 'sub' from claims. " 272 | "'sub' is your admin email as a mailto: link.") 273 | if not re.match(r"^https?://[^/:]+(:\d+)?$", 274 | cclaims.get("aud", ""), 275 | re.IGNORECASE): 276 | raise VapidException( 277 | "Missing 'aud' from claims. " 278 | "'aud' is the scheme, host and optional port for this " 279 | "transaction e.g. https://example.com:8080") 280 | return cclaims 281 | 282 | def sign(self, claims, crypto_key=None): 283 | """Sign a set of claims. 284 | :param claims: JSON object containing the JWT claims to use. 285 | :type claims: dict 286 | :param crypto_key: Optional existing crypto_key header content. The 287 | vapid public key will be appended to this data. 288 | :type crypto_key: str 289 | :returns: a hash containing the header fields to use in 290 | the subscription update. 291 | :rtype: dict 292 | 293 | """ 294 | sig = sign(self._base_sign(claims), self.private_key) 295 | pkey = 'p256ecdsa=' 296 | pkey += b64urlencode( 297 | self.public_key.public_bytes( 298 | serialization.Encoding.X962, 299 | serialization.PublicFormat.UncompressedPoint 300 | )) 301 | if crypto_key: 302 | crypto_key = crypto_key + ';' + pkey 303 | else: 304 | crypto_key = pkey 305 | 306 | return {"Authorization": "{} {}".format(self._schema, sig.strip('=')), 307 | "Crypto-Key": crypto_key} 308 | 309 | 310 | class Vapid02(Vapid01): 311 | """Minimal Vapid RFC8292 signature generation library 312 | 313 | https://tools.ietf.org/html/rfc8292 314 | 315 | """ 316 | _schema = "vapid" 317 | 318 | def sign(self, claims, crypto_key=None): 319 | """Generate an authorization token 320 | 321 | :param claims: JSON object containing the JWT claims to use. 322 | :type claims: dict 323 | :param crypto_key: Optional existing crypto_key header content. The 324 | vapid public key will be appended to this data. 325 | :type crypto_key: str 326 | :returns: a hash containing the header fields to use in 327 | the subscription update. 328 | :rtype: dict 329 | """ 330 | sig = sign(self._base_sign(claims), self.private_key) 331 | pkey = self.public_key.public_bytes( 332 | serialization.Encoding.X962, 333 | serialization.PublicFormat.UncompressedPoint 334 | ) 335 | return{ 336 | "Authorization": "{schema} t={t},k={k}".format( 337 | schema=self._schema, 338 | t=sig, 339 | k=b64urlencode(pkey) 340 | ) 341 | } 342 | 343 | @classmethod 344 | def verify(cls, auth): 345 | """Ensure that the token is correctly formatted and valid 346 | 347 | :param auth: An Authorization header 348 | :type auth: str 349 | :rtype: bool 350 | 351 | """ 352 | pref_tok = auth.rsplit(' ', 1) 353 | assert pref_tok[0].lower() == cls._schema, ( 354 | "Incorrect schema specified") 355 | parts = {} 356 | for tok in pref_tok[1].split(','): 357 | kv = tok.split('=', 1) 358 | parts[kv[0]] = kv[1] 359 | assert 'k' in parts.keys(), ( 360 | "Auth missing public key 'k' value") 361 | assert 't' in parts.keys(), ( 362 | "Auth missing token set 't' value") 363 | kp = cls().from_raw_public(parts['k'].encode()) 364 | tokens = parts['t'].rsplit('.', 1) 365 | return kp.verify_token( 366 | validation_token=tokens[0].encode(), 367 | verification_token=tokens[1] 368 | ) 369 | 370 | 371 | def _check_sub(sub): 372 | """ Check to see if the `sub` is a properly formatted `mailto:` 373 | 374 | a `mailto:` should be a SMTP mail address. Mind you, since I run 375 | YouFailAtEmail.com, you have every right to yell about how terrible 376 | this check is. I really should be doing a proper component parse 377 | and valiate each component individually per RFC5341, instead I do 378 | the unholy regex you see below. 379 | 380 | :param sub: Candidate JWT `sub` 381 | :type sub: str 382 | :rtype: bool 383 | 384 | """ 385 | pattern = ( 386 | 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 387 | ) 388 | return re.match(pattern, sub, re.IGNORECASE) is not None 389 | 390 | 391 | Vapid = Vapid02 392 | -------------------------------------------------------------------------------- /python/py_vapid/jwt.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import json 3 | 4 | from cryptography.exceptions import InvalidSignature 5 | from cryptography.hazmat.primitives.asymmetric import ec, utils 6 | from cryptography.hazmat.primitives import hashes 7 | 8 | from py_vapid.utils import b64urldecode, b64urlencode, num_to_bytes 9 | 10 | 11 | def extract_signature(auth): 12 | """Extracts the payload and signature from a JWT, converting from RFC7518 13 | to RFC 3279 14 | 15 | :param auth: A JWT Authorization Token. 16 | :type auth: str 17 | 18 | :return tuple containing the signature material and signature 19 | 20 | """ 21 | payload, asig = auth.encode('utf8').rsplit(b'.', 1) 22 | sig = b64urldecode(asig) 23 | if len(sig) != 64: 24 | raise InvalidSignature() 25 | 26 | encoded = utils.encode_dss_signature( 27 | s=int(binascii.hexlify(sig[32:]), 16), 28 | r=int(binascii.hexlify(sig[:32]), 16) 29 | ) 30 | return payload, encoded 31 | 32 | 33 | def decode(token, key): 34 | """Decode a web token into an assertion dictionary 35 | 36 | :param token: VAPID auth token 37 | :type token: str 38 | :param key: bitarray containing the public key 39 | :type key: str 40 | 41 | :return dict of the VAPID claims 42 | 43 | :raise InvalidSignature 44 | 45 | """ 46 | try: 47 | sig_material, signature = extract_signature(token) 48 | dkey = b64urldecode(key.encode('utf8')) 49 | pkey = ec.EllipticCurvePublicKey.from_encoded_point( 50 | ec.SECP256R1(), 51 | dkey, 52 | ) 53 | pkey.verify( 54 | signature, 55 | sig_material, 56 | ec.ECDSA(hashes.SHA256()) 57 | ) 58 | return json.loads( 59 | b64urldecode(sig_material.split(b'.')[1]).decode('utf8') 60 | ) 61 | except InvalidSignature: 62 | raise 63 | except(ValueError, TypeError, binascii.Error): 64 | raise InvalidSignature() 65 | 66 | 67 | def sign(claims, key): 68 | """Sign the claims 69 | 70 | :param claims: list of JWS claims 71 | :type claims: dict 72 | :param key: Private key for signing 73 | :type key: ec.EllipticCurvePrivateKey 74 | :param algorithm: JWT "alg" descriptor 75 | :type algorithm: str 76 | 77 | """ 78 | header = b64urlencode(b"""{"typ":"JWT","alg":"ES256"}""") 79 | # Unfortunately, chrome seems to require the claims to be sorted. 80 | claims = b64urlencode(json.dumps(claims, 81 | separators=(',', ':'), 82 | sort_keys=True).encode('utf8')) 83 | token = "{}.{}".format(header, claims) 84 | rsig = key.sign(token.encode('utf8'), ec.ECDSA(hashes.SHA256())) 85 | (r, s) = utils.decode_dss_signature(rsig) 86 | sig = b64urlencode(num_to_bytes(r, 32) + num_to_bytes(s, 32)) 87 | return "{}.{}".format(token, sig) 88 | -------------------------------------------------------------------------------- /python/py_vapid/main.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import argparse 6 | import os 7 | import json 8 | 9 | from cryptography.hazmat.primitives import serialization 10 | 11 | from py_vapid import Vapid01, Vapid02, b64urlencode 12 | 13 | 14 | def prompt(prompt): 15 | # Not sure why, but python3 throws and exception if you try to 16 | # monkeypatch for this. It's ugly, but this seems to play nicer. 17 | try: 18 | return input(prompt) 19 | except NameError: 20 | return raw_input(prompt) # noqa: F821 21 | 22 | 23 | def main(): 24 | parser = argparse.ArgumentParser(description="VAPID tool") 25 | parser.add_argument('--sign', '-s', help='claims file to sign') 26 | parser.add_argument('--gen', '-g', help='generate new key pairs', 27 | default=False, action="store_true") 28 | parser.add_argument('--version2', '-2', help="use RFC8292 VAPID spec", 29 | default=True, action="store_true") 30 | parser.add_argument('--version1', '-1', help="use VAPID spec Draft-01", 31 | default=False, action="store_true") 32 | parser.add_argument('--json', help="dump as json", 33 | default=False, action="store_true") 34 | parser.add_argument('--no-strict', help='Do not be strict about "sub"', 35 | default=False, action="store_true") 36 | parser.add_argument('--applicationServerKey', 37 | help="show applicationServerKey value", 38 | default=False, action="store_true") 39 | parser.add_argument('--private-key', '-k', help='private key pem file', 40 | default="private_key.pem") 41 | args = parser.parse_args() 42 | 43 | # Added to solve 2.7 => 3.* incompatibility 44 | Vapid = Vapid02 45 | if args.version1: 46 | Vapid = Vapid01 47 | if args.gen or not os.path.exists(args.private_key): 48 | if not args.gen: 49 | print("No private key file found.") 50 | answer = None 51 | while answer not in ['y', 'n']: 52 | answer = prompt("Do you want me to create one for you? (Y/n)") 53 | if not answer: 54 | answer = 'y' 55 | answer = answer.lower()[0] 56 | if answer == 'n': 57 | print("Sorry, can't do much for you then.") 58 | exit(1) 59 | vapid = Vapid(conf=args) 60 | vapid.generate_keys() 61 | print("Generating private_key.pem") 62 | vapid.save_key('private_key.pem') 63 | print("Generating public_key.pem") 64 | vapid.save_public_key('public_key.pem') 65 | vapid = Vapid.from_file(args.private_key) 66 | claim_file = args.sign 67 | result = dict() 68 | if args.applicationServerKey: 69 | raw_pub = vapid.public_key.public_bytes( 70 | serialization.Encoding.X962, 71 | serialization.PublicFormat.UncompressedPoint 72 | ) 73 | print("Application Server Key = {}\n\n".format( 74 | b64urlencode(raw_pub))) 75 | if claim_file: 76 | if not os.path.exists(claim_file): 77 | print("No {} file found.".format(claim_file)) 78 | print(""" 79 | The claims file should be a JSON formatted file that holds the 80 | information that describes you. There are three elements in the claims 81 | file you'll need: 82 | 83 | "sub" This is your site's admin email address 84 | (e.g. "mailto:admin@example.com") 85 | "exp" This is the expiration time for the claim in seconds. If you don't 86 | have one, I'll add one that expires in 24 hours. 87 | 88 | You're also welcome to add additional fields to the claims which could be 89 | helpful for the Push Service operations team to pass along to your operations 90 | team (e.g. "ami-id": "e-123456", "cust-id": "a3sfa10987"). Remember to keep 91 | these values short to prevent some servers from rejecting the transaction due 92 | to overly large headers. See https://jwt.io/introduction/ for details. 93 | 94 | For example, a claims.json file could contain: 95 | 96 | {"sub": "mailto:admin@example.com"} 97 | """) 98 | exit(1) 99 | try: 100 | claims = json.loads(open(claim_file).read()) 101 | result.update(vapid.sign(claims)) 102 | except Exception as exc: 103 | print("Crap, something went wrong: {}".format(repr(exc))) 104 | raise exc 105 | if args.json: 106 | print(json.dumps(result)) 107 | return 108 | print("Include the following headers in your request:\n") 109 | for key, value in result.items(): 110 | print("{}: {}\n".format(key, value)) 111 | print("\n") 112 | 113 | 114 | if __name__ == '__main__': 115 | main() 116 | -------------------------------------------------------------------------------- /python/py_vapid/tests/test_vapid.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import base64 3 | import copy 4 | import os 5 | import json 6 | import unittest 7 | from cryptography.hazmat.primitives import serialization 8 | from mock import patch, Mock 9 | 10 | from py_vapid import Vapid01, Vapid02, VapidException, _check_sub 11 | from py_vapid.jwt import decode 12 | 13 | TEST_KEY_PRIVATE_DER = """ 14 | MHcCAQEEIPeN1iAipHbt8+/KZ2NIF8NeN24jqAmnMLFZEMocY8RboAoGCCqGSM49 15 | AwEHoUQDQgAEEJwJZq/GN8jJbo1GGpyU70hmP2hbWAUpQFKDByKB81yldJ9GTklB 16 | M5xqEwuPM7VuQcyiLDhvovthPIXx+gsQRQ== 17 | """ 18 | 19 | key = dict( 20 | d=111971876876285331364078054667935803036831194031221090723024134705696601261147, # noqa 21 | x=7512698603580564493364310058109115206932767156853859985379597995200661812060, # noqa 22 | y=74837673548863147047276043384733294240255217876718360423043754089982135570501 # noqa 23 | ) 24 | 25 | # This is the same private key, in PEM form. 26 | TEST_KEY_PRIVATE_PEM = ( 27 | "-----BEGIN PRIVATE KEY-----{}" 28 | "-----END PRIVATE KEY-----\n").format(TEST_KEY_PRIVATE_DER) 29 | 30 | # This is the same private key, as a point in uncompressed form. This should 31 | # be Base64url-encoded without padding. 32 | TEST_KEY_PRIVATE_RAW = """ 33 | 943WICKkdu3z78pnY0gXw143biOoCacwsVkQyhxjxFs 34 | """.strip().encode('utf8') 35 | 36 | # This is a public key in PEM form. 37 | TEST_KEY_PUBLIC_PEM = """-----BEGIN PUBLIC KEY----- 38 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEJwJZq/GN8jJbo1GGpyU70hmP2hb 39 | WAUpQFKDByKB81yldJ9GTklBM5xqEwuPM7VuQcyiLDhvovthPIXx+gsQRQ== 40 | -----END PUBLIC KEY----- 41 | """ 42 | 43 | # this is a public key in uncompressed form ('\x04' + 2 * 32 octets) 44 | # Remember, this should have any padding stripped. 45 | TEST_KEY_PUBLIC_RAW = ( 46 | "BBCcCWavxjfIyW6NRhqclO9IZj9oW1gFKUBSgwcigfNc" 47 | "pXSfRk5JQTOcahMLjzO1bkHMoiw4b6L7YTyF8foLEEU" 48 | ).strip('=').encode('utf8') 49 | 50 | 51 | def setup_module(self): 52 | with open('/tmp/private', 'w') as ff: 53 | ff.write(TEST_KEY_PRIVATE_PEM) 54 | with open('/tmp/public', 'w') as ff: 55 | ff.write(TEST_KEY_PUBLIC_PEM) 56 | with open('/tmp/private.der', 'w') as ff: 57 | ff.write(TEST_KEY_PRIVATE_DER) 58 | 59 | 60 | def teardown_module(self): 61 | os.unlink('/tmp/private') 62 | os.unlink('/tmp/public') 63 | 64 | 65 | class VapidTestCase(unittest.TestCase): 66 | def check_keys(self, v): 67 | assert v.private_key.private_numbers().private_value == key.get('d') 68 | assert v.public_key.public_numbers().x == key.get('x') 69 | assert v.public_key.public_numbers().y == key.get('y') 70 | 71 | def test_init(self): 72 | v1 = Vapid01.from_file("/tmp/private") 73 | self.check_keys(v1) 74 | v2 = Vapid01.from_pem(TEST_KEY_PRIVATE_PEM.encode()) 75 | self.check_keys(v2) 76 | v3 = Vapid01.from_der(TEST_KEY_PRIVATE_DER.encode()) 77 | self.check_keys(v3) 78 | v4 = Vapid01.from_file("/tmp/private.der") 79 | self.check_keys(v4) 80 | no_exist = '/tmp/not_exist' 81 | Vapid01.from_file(no_exist) 82 | assert os.path.isfile(no_exist) 83 | os.unlink(no_exist) 84 | 85 | def repad(self, data): 86 | return data + "===="[len(data) % 4:] 87 | 88 | @patch("py_vapid.Vapid01.from_pem", side_effect=Exception) 89 | def test_init_bad_read(self, mm): 90 | self.assertRaises(Exception, 91 | Vapid01.from_file, 92 | private_key_file="/tmp/private") 93 | 94 | def test_gen_key(self): 95 | v = Vapid01() 96 | v.generate_keys() 97 | assert v.public_key 98 | assert v.private_key 99 | 100 | def test_private_key(self): 101 | v = Vapid01() 102 | self.assertRaises(VapidException, 103 | lambda: v.private_key) 104 | 105 | def test_public_key(self): 106 | v = Vapid01() 107 | assert v._private_key is None 108 | assert v._public_key is None 109 | 110 | def test_save_key(self): 111 | v = Vapid01() 112 | v.generate_keys() 113 | v.save_key("/tmp/p2") 114 | os.unlink("/tmp/p2") 115 | 116 | def test_same_public_key(self): 117 | v = Vapid01() 118 | v.generate_keys() 119 | v.save_public_key("/tmp/p2") 120 | os.unlink("/tmp/p2") 121 | 122 | def test_from_raw(self): 123 | v = Vapid01.from_raw(TEST_KEY_PRIVATE_RAW) 124 | self.check_keys(v) 125 | 126 | def test_from_string(self): 127 | v1 = Vapid01.from_string(TEST_KEY_PRIVATE_DER) 128 | v2 = Vapid01.from_string(TEST_KEY_PRIVATE_RAW.decode()) 129 | self.check_keys(v1) 130 | self.check_keys(v2) 131 | 132 | def test_sign_01(self): 133 | v = Vapid01.from_string(TEST_KEY_PRIVATE_DER) 134 | claims = {"aud": "https://example.com", 135 | "sub": "mailto:admin@example.com"} 136 | result = v.sign(claims, "id=previous") 137 | assert result['Crypto-Key'] == ( 138 | 'id=previous;p256ecdsa=' + TEST_KEY_PUBLIC_RAW.decode('utf8')) 139 | pkey = binascii.b2a_base64( 140 | v.public_key.public_bytes( 141 | serialization.Encoding.X962, 142 | serialization.PublicFormat.UncompressedPoint 143 | ) 144 | ).decode('utf8').replace('+', '-').replace('/', '_').strip() 145 | items = decode(result['Authorization'].split(' ')[1], pkey) 146 | for k in claims: 147 | assert items[k] == claims[k] 148 | result = v.sign(claims) 149 | assert result['Crypto-Key'] == ( 150 | 'p256ecdsa=' + TEST_KEY_PUBLIC_RAW.decode('utf8')) 151 | # Verify using the same function as Integration 152 | # this should ensure that the r,s sign values are correctly formed 153 | assert Vapid01.verify( 154 | key=result['Crypto-Key'].split('=')[1], 155 | auth=result['Authorization'] 156 | ) 157 | 158 | def test_sign_02(self): 159 | v = Vapid02.from_file("/tmp/private") 160 | claims = {"aud": "https://example.com", 161 | "sub": "mailto:admin@example.com", 162 | "foo": "extra value"} 163 | claim_check = copy.deepcopy(claims) 164 | result = v.sign(claims, "id=previous") 165 | auth = result['Authorization'] 166 | assert auth[:6] == 'vapid ' 167 | assert ' t=' in auth 168 | assert ',k=' in auth 169 | parts = auth[6:].split(',') 170 | assert len(parts) == 2 171 | t_val = json.loads(base64.urlsafe_b64decode( 172 | self.repad(parts[0][2:].split('.')[1]) 173 | ).decode('utf8')) 174 | k_val = binascii.a2b_base64(self.repad(parts[1][2:])) 175 | assert binascii.hexlify(k_val)[:2] == b'04' 176 | assert len(k_val) == 65 177 | assert claims == claim_check 178 | for k in claims: 179 | assert t_val[k] == claims[k] 180 | 181 | def test_sign_02_localhost(self): 182 | v = Vapid02.from_file("/tmp/private") 183 | claims = {"aud": "http://localhost:8000", 184 | "sub": "mailto:admin@example.com", 185 | "foo": "extra value"} 186 | result = v.sign(claims, "id=previous") 187 | auth = result['Authorization'] 188 | assert auth[:6] == 'vapid ' 189 | assert ' t=' in auth 190 | assert ',k=' in auth 191 | 192 | def test_integration(self): 193 | # These values were taken from a test page. DO NOT ALTER! 194 | key = ("BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foI" 195 | "iBHXRdJI2Qhumhf6_LFTeZaNndIo") 196 | auth = ("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJod" 197 | "HRwczovL3VwZGF0ZXMucHVzaC5zZXJ2aWNlcy5tb3ppbGxhLmNvbSIsImV" 198 | "4cCI6MTQ5NDY3MTQ3MCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlb" 199 | "W9AZ2F1bnRmYWNlLmNvLnVrIn0.LqPi86T-HJ71TXHAYFptZEHD7Wlfjcc" 200 | "4u5jYZ17WpqOlqDcW-5Wtx3x1OgYX19alhJ9oLumlS2VzEvNioZolQA") 201 | assert Vapid01.verify(key=key, auth="webpush {}".format(auth)) 202 | assert Vapid02.verify(auth="vapid t={},k={}".format(auth, key)) 203 | 204 | def test_bad_integration(self): 205 | # These values were taken from a test page. DO NOT ALTER! 206 | key = ("BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foI" 207 | "iBHXRdJI2Qhumhf6_LFTeZaNndIo") 208 | auth = ("WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJod" 209 | "HRwczovL3VwZGF0ZXMucHVzaC5zZXJ2aWNlcy5tb3ppbGxhLmNvbSIsImV" 210 | "4cCI6MTQ5NDY3MTQ3MCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlb" 211 | "W9AZ2F1bnRmYWNlLmNvLnVrIn0.LqPi86T-HJ71TXHAYFptZEHD7Wlfjcc" 212 | "4u5jYZ17WpqOlqDcW-5Wtx3x1OgYX19alhJ9oLumlS2VzEvNioZ_BAD") 213 | assert not Vapid01.verify(key=key, auth=auth) 214 | 215 | def test_bad_sign(self): 216 | v = Vapid01.from_file("/tmp/private") 217 | self.assertRaises(VapidException, 218 | v.sign, 219 | {}) 220 | self.assertRaises(VapidException, 221 | v.sign, 222 | {'sub': 'foo', 223 | 'aud': "p.example.com"}) 224 | self.assertRaises(VapidException, 225 | v.sign, 226 | {'sub': 'mailto:foo@bar.com', 227 | 'aud': "p.example.com"}) 228 | self.assertRaises(VapidException, 229 | v.sign, 230 | {'sub': 'mailto:foo@bar.com', 231 | 'aud': "https://p.example.com:8080/"}) 232 | 233 | def test_ignore_sub(self): 234 | v = Vapid02.from_file("/tmp/private") 235 | v.conf['no-strict'] = True 236 | assert v.sign({"sub": "foo", "aud": "http://localhost:8000"}) 237 | 238 | @patch('cryptography.hazmat.primitives.asymmetric' 239 | '.ec.EllipticCurvePublicNumbers') 240 | def test_invalid_sig(self, mm): 241 | from cryptography.exceptions import InvalidSignature 242 | ve = Mock() 243 | ve.verify.side_effect = InvalidSignature 244 | pk = Mock() 245 | pk.public_key.return_value = ve 246 | mm.from_encoded_point.return_value = pk 247 | self.assertRaises(InvalidSignature, 248 | decode, 249 | 'foo.bar.blat', 250 | 'aaaa') 251 | self.assertRaises(InvalidSignature, 252 | decode, 253 | 'foo.bar.a', 254 | 'aaaa') 255 | 256 | def test_sub(self): 257 | valid = [ 258 | 'mailto:me@localhost', 259 | 'mailto:me@1.2.3.4', 260 | 'mailto:me@1234::', 261 | 'mailto:me@1234::5678', 262 | 'mailto:admin@example.org', 263 | 'mailto:admin-test-case@example-test-case.test.org', 264 | 'https://localhost', 265 | 'https://exmample-test-case.test.org', 266 | 'https://8001::', 267 | 'https://8001:1000:0001', 268 | 'https://1.2.3.4' 269 | ] 270 | invalid = [ 271 | 'mailto:@foobar.com', 272 | 'mailto:example.org', 273 | 'mailto:0123:', 274 | 'mailto:::1234', 275 | 'https://somehost', 276 | 'https://xyz:123', 277 | ] 278 | 279 | for val in valid: 280 | assert _check_sub(val) is True 281 | for val in invalid: 282 | assert _check_sub(val) is False 283 | -------------------------------------------------------------------------------- /python/py_vapid/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import binascii 3 | 4 | 5 | def b64urldecode(data): 6 | """Decodes an unpadded Base64url-encoded string. 7 | 8 | :param data: data bytes to decode 9 | :type data: bytes 10 | 11 | :returns bytes 12 | 13 | """ 14 | return base64.urlsafe_b64decode(data + b"===="[len(data) % 4:]) 15 | 16 | 17 | def b64urlencode(data): 18 | """Encode a byte string into a Base64url-encoded string without padding 19 | 20 | :param data: data bytes to encode 21 | :type data: bytes 22 | 23 | :returns str 24 | 25 | """ 26 | return base64.urlsafe_b64encode(data).replace(b'=', b'').decode('utf8') 27 | 28 | 29 | def num_to_bytes(n, pad_to): 30 | """Returns the byte representation of an integer, in big-endian order. 31 | :param n: The integer to encode. 32 | :type n: int 33 | :param pad_to: Expected length of result, zeropad if necessary. 34 | :type pad_to: int 35 | :returns bytes 36 | """ 37 | h = '%x' % n 38 | r = binascii.unhexlify('0' * (len(h) % 2) + h) 39 | return b'\x00' * (pad_to - len(r)) + r 40 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "py-vapid" 7 | version = "1.9.2" 8 | license = {text = "MPL-2.0"} 9 | description = "Simple VAPID header generation library" 10 | readme = "README.rst" 11 | authors = [{name = "JR Conlin", email = "src+vapid@jrconlin.com"}] 12 | keywords = ["vapid", "push", "webpush"] 13 | classifiers = [ 14 | "Topic :: Internet :: WWW/HTTP", 15 | "Programming Language :: Python", 16 | "Programming Language :: Python :: 3", 17 | ] 18 | dynamic = ["dependencies"] 19 | 20 | [project.urls] 21 | Homepage = "https://github.com/mozilla-services/vapid" 22 | 23 | [project.scripts] 24 | vapid = "py_vapid.main:main" 25 | 26 | [tool.setuptools.dynamic] 27 | dependencies = {file = "requirements.txt"} 28 | 29 | [tool.setuptools.packages.find] 30 | include = ["py_vapid*"] 31 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography>=2.5 2 | -------------------------------------------------------------------------------- /python/setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbose=True 3 | verbosity=1 4 | #cover-tests=True 5 | #cover-erase=True 6 | #with-coverage=True 7 | detailed-errors=True 8 | #cover-package=py_vapid 9 | -------------------------------------------------------------------------------- /python/test-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pytest 3 | coverage 4 | mock>=1.0.1 5 | flake8 6 | -------------------------------------------------------------------------------- /python/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Package the current branch up to pypi 3 | # remember to update the README.rst file 4 | #pandoc --from=markdown --to=rst --output README.rst README.md 5 | #pandoc --from=markdown --to=rst --output CHANGELOG.rst CHANGELOG.md 6 | venv/bin/python -m pip install --upgrade build 7 | venv/bin/python -m build 8 | venv/bin/twine upload dist/* --verbose 9 | -------------------------------------------------------------------------------- /rust/vapid/.gitignore: -------------------------------------------------------------------------------- 1 | .floo* 2 | .idea/ 3 | -------------------------------------------------------------------------------- /rust/vapid/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.0 2 | 3 | * Changed `VapidErrors` to be more Clippy friendly 4 | * updates for latest rust 5 | 6 | 7 | # 0.2.0 8 | 9 | Due to changes in the OpenSSL library, several calls changed form from `0.1.0` 10 | 11 | Most calls now return as a `Result<_, VapidError>`. `VapidError` is a type of `failure` call, so it should make logging and error handling a bit easier. 12 | 13 | This does mean that 14 | 15 | ```rust, no_run 16 | let key = Key::generate(); 17 | ``` 18 | is now 19 | ```rust, no_run 20 | let key = Key.generate().unwrap(); 21 | ``` 22 | 23 | The `.group()` method for `Key` has been removed. It was a convenience function. You can replace it with generating the group directly 24 | `ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?` 25 | 26 | There are now `VapidErrors` -------------------------------------------------------------------------------- /rust/vapid/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /rust/vapid/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Anyone is welcome to contribute to this project. Feel free to get in touch with 4 | other community members on IRC, the mailing list or through issues here on 5 | GitHub. 6 | 7 | [See the README](/README.md) for contact information. 8 | 9 | ## Bug Reports 10 | 11 | You can file issues here on GitHub. Please try to include as much information as 12 | you can and under what conditions you saw the issue. 13 | 14 | ## Sending Pull Requests 15 | 16 | Patches should be submitted as pull requests (PR). 17 | 18 | Before submitting a PR: 19 | - Your code must run and pass all the automated tests before you submit your PR 20 | for review. "Work in progress" pull requests are allowed to be submitted, but 21 | should be clearly labeled as such and should not be merged until all tests 22 | pass and the code has been reviewed. 23 | - Your patch should include new tests that cover your changes. It is your and 24 | your reviewer's responsibility to ensure your patch includes adequate tests. 25 | 26 | When submitting a PR: 27 | - You agree to license your code under the project's open source license 28 | ([MPL 2.0](/LICENSE)). 29 | - Base your branch off the current `master`. 30 | - Add both your code and new tests if relevant. 31 | - Sign your git commit. 32 | - Run the test suite to make sure your code passes linting and tests. 33 | - Ensure your changes do not reduce code coverage of the test suite. 34 | - Please do not include merge commits in pull requests; include only commits 35 | with the new relevant code. 36 | 37 | ## Code Review 38 | 39 | This project is production Mozilla code and subject to our [engineering practices and quality standards](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities). Every patch must be peer reviewed. 40 | 41 | ## Git Commit Guidelines 42 | 43 | We loosely follow the [Angular commit guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#type) 44 | of `: ` where `type` must be one of: 45 | 46 | * **feat**: A new feature 47 | * **fix**: A bug fix 48 | * **docs**: Documentation only changes 49 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 50 | semi-colons, etc) 51 | * **refactor**: A code change that neither fixes a bug or adds a feature 52 | * **perf**: A code change that improves performance 53 | * **test**: Adding missing tests 54 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 55 | generation 56 | * **breaks**: Contains a *BREAKING_CHANGE* to the existing execution environment. 57 | 58 | ### Subject 59 | 60 | The subject contains succinct description of the change: 61 | 62 | * use the imperative, present tense: "change" not "changed" nor "changes" 63 | * don't capitalize first letter 64 | * no dot (.) at the end 65 | 66 | ### Body 67 | 68 | In order to maintain a reference to the context of the commit, add 69 | `Closes #` if it closes a related issue or `Issue #` 70 | if it's a partial fix. 71 | 72 | You can also write a detailed description of the commit: Just as in the 73 | **subject**, use the imperative, present tense: "change" not "changed" nor 74 | "changes" It should include the motivation for the change and contrast this with 75 | previous behavior. 76 | 77 | ### Footer 78 | 79 | The footer should contain any information about **Breaking Changes** and is also 80 | the place to reference GitHub issues that this commit **Closes**. 81 | 82 | ### Example 83 | 84 | A properly formatted commit message should look like: 85 | 86 | ``` 87 | feat: give the developers a delicious cookie 88 | 89 | Properly formatted commit messages provide understandable history and 90 | documentation. This patch will provide a delicious cookie when all tests have 91 | passed and the commit message is properly formatted. 92 | 93 | BREAKING CHANGE: This patch requires developer to lower expectations about 94 | what "delicious" and "cookie" may mean. Some sadness may result. 95 | 96 | Closes #3.14, #9.75 97 | ``` 98 | -------------------------------------------------------------------------------- /rust/vapid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vapid" 3 | version = "0.6.0" 4 | authors = ["jrconlin "] 5 | edition = "2021" 6 | description = "An implementation of the RFC 8292 Voluntary Application Server Identification (VAPID) Auth header generator" 7 | repository = "https://github.com/web-push-libs/vapid" 8 | license = "MPL-2.0" 9 | 10 | [dependencies] 11 | backtrace="0.3" 12 | openssl = "0.10" 13 | serde_json = "1.0" 14 | base64 = "0.13" 15 | time = "0.3" 16 | thiserror = "1.0" 17 | -------------------------------------------------------------------------------- /rust/vapid/LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /rust/vapid/src/error.rs: -------------------------------------------------------------------------------- 1 | // Error handling based on the failure crate 2 | 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::result; 6 | 7 | use backtrace::Backtrace; 8 | use thiserror::Error; 9 | 10 | pub type VapidResult = result::Result; 11 | 12 | #[derive(Debug)] 13 | pub struct VapidError { 14 | kind: VapidErrorKind, 15 | pub backtrace: Backtrace, 16 | } 17 | 18 | #[derive(Debug, Error)] 19 | pub enum VapidErrorKind { 20 | /// General IO instance. Can be returned for bad files or key data. 21 | #[error("IO error: {:?}", .0)] 22 | File(#[from] std::io::Error), 23 | /// OpenSSL errors. These tend not to be very specific (or helpful). 24 | #[error("OpenSSL error: {:?}", .0)] 25 | OpenSSL(#[from] openssl::error::ErrorStack), 26 | /// JSON parsing error. 27 | #[error("JSON error:{:?}", .0)] 28 | Json(#[from] serde_json::Error), 29 | 30 | /// An invalid public key was specified. Is it EC Prime256v1? 31 | #[error("Invalid public key")] 32 | PublicKey, 33 | /// A vapid error occurred. 34 | #[error("VAPID error: {}", .0)] 35 | Protocol(String), 36 | /// A random internal error 37 | #[error("Internal Error {:?}", .0)] 38 | Internal(String), 39 | } 40 | 41 | /// VapidErrors are the general error wrapper that we use. These include 42 | /// a public `backtrace` which can be combined with your own because they're 43 | /// stupidly useful. 44 | impl VapidError { 45 | pub fn kind(&self) -> &VapidErrorKind { 46 | &self.kind 47 | } 48 | 49 | pub fn internal(msg: &str) -> Self { 50 | VapidErrorKind::Internal(msg.to_owned()).into() 51 | } 52 | } 53 | 54 | impl From for VapidError 55 | where 56 | VapidErrorKind: From, 57 | { 58 | fn from(item: T) -> Self { 59 | VapidError { 60 | kind: VapidErrorKind::from(item), 61 | backtrace: Backtrace::new(), 62 | } 63 | } 64 | } 65 | 66 | impl Error for VapidError { 67 | fn source(&self) -> Option<&(dyn Error + 'static)> { 68 | self.kind.source() 69 | } 70 | } 71 | 72 | impl fmt::Display for VapidError { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | self.kind.fmt(f) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rust/vapid/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! VAPID auth support 2 | //! 3 | //! This library only supports the latest VAPID-draft-02+ specification. 4 | //! 5 | //! Example Use: 6 | //! ```rust,no_run 7 | //! use vapid::{Key, sign}; 8 | //! use std::collections::HashMap; 9 | //! 10 | //! // Create a key from an existing EC Private Key PEM file. 11 | //! // You can generate this with 12 | //! // Key::generate().to_pem("pem/file/path.pem"); 13 | //! let my_key = Key::from_pem("pem/file/path.pem").unwrap(); 14 | //! 15 | //! // Construct the Claims hashmap 16 | //! let mut claims:HashMap = HashMap::new(); 17 | //! claims.insert( 18 | //! String::from("sub"), serde_json::Value::from("mailto:bob@example.com") 19 | //! ); 20 | //! // while `exp` can be filled in for you, `aud` should point to the net location of the 21 | //! // Push server you wish to talk to. (e.g. `https://push.services.mozilla.org`) 22 | //! // `aud` is optional for Mozilla, but may be required for GCM/FCM or other systems. 23 | //! claims.insert( 24 | //! String::from("aud"), serde_json::Value::from("https://host.ext") 25 | //! ); 26 | //! 27 | //! // The result will contain the `Authorization:` header. How you inject this into your 28 | //! // request is left as an exercise. 29 | //! let authorization_header = sign(my_key, &mut claims).unwrap(); 30 | //! 31 | //! ``` 32 | 33 | use std::time::SystemTime; 34 | 35 | use std::collections::HashMap; 36 | use std::fs; 37 | use std::hash::BuildHasher; 38 | use std::path::Path; 39 | 40 | use openssl::bn::BigNumContext; 41 | use openssl::ec::{self, EcKey}; 42 | use openssl::hash::MessageDigest; 43 | use openssl::nid; 44 | use openssl::pkey::{PKey, Private, Public}; 45 | use openssl::sign::{Signer, Verifier}; 46 | 47 | mod error; 48 | 49 | /// a Key is a helper for creating or using a VAPID EC key. 50 | /// 51 | /// Vapid Keys are always Prime256v1 EC keys. 52 | /// 53 | pub struct Key { 54 | key: EcKey, 55 | } 56 | 57 | impl Key { 58 | /// return the name of the key. 59 | /// It's always going to be this static value (for now). 60 | /// Eventually it might be "Kevin", but let's not dwell on that. 61 | fn name() -> nid::Nid { 62 | nid::Nid::X9_62_PRIME256V1 63 | } 64 | 65 | /// Read a VAPID private key in PEM format stored in `path` 66 | pub fn from_pem

(path: P) -> error::VapidResult 67 | where 68 | P: AsRef, 69 | { 70 | let pem_data = fs::read(&path)?; 71 | Ok(Key { 72 | key: PKey::private_key_from_pem(&pem_data)?.ec_key().unwrap(), 73 | }) 74 | } 75 | 76 | /// Write the VAPID private key as a PEM to `path` 77 | pub fn to_pem(&self, path: &Path) -> error::VapidResult<()> { 78 | let key_data: Vec = self.key.private_key_to_pem()?; 79 | fs::write(&path, &key_data)?; 80 | Ok(()) 81 | } 82 | 83 | /// Create a new Vapid key 84 | pub fn generate() -> error::VapidResult { 85 | let group = ec::EcGroup::from_curve_name(Key::name())?; 86 | let key = ec::EcKey::generate(&group)?; 87 | Ok(Key { key }) 88 | } 89 | 90 | /// Convert the private key into a base64 string 91 | pub fn to_private_raw(&self) -> String { 92 | // Return the private key as a raw bit array 93 | let key = self.key.private_key(); 94 | base64::encode_config(&key.to_vec(), base64::URL_SAFE_NO_PAD) 95 | } 96 | 97 | /// Convert the public key into a uncompressed, raw base64 string 98 | pub fn to_public_raw(&self) -> String { 99 | //Return the public key as a raw bit array 100 | let mut ctx = BigNumContext::new().unwrap(); 101 | let group = ec::EcGroup::from_curve_name(Key::name()).unwrap(); 102 | 103 | let key = self.key.public_key(); 104 | let keybytes = key 105 | .to_bytes(&group, ec::PointConversionForm::UNCOMPRESSED, &mut ctx) 106 | .unwrap(); 107 | base64::encode_config(&keybytes, base64::URL_SAFE_NO_PAD) 108 | } 109 | 110 | /// Read the public key from an uncompressed, raw base64 string 111 | pub fn from_public_raw(bits: String) -> error::VapidResult> { 112 | //Read a public key from a raw bit array 113 | let bytes: Vec = 114 | base64::decode_config(&bits.into_bytes(), base64::URL_SAFE_NO_PAD).unwrap(); 115 | let mut ctx = BigNumContext::new().unwrap(); 116 | let group = ec::EcGroup::from_curve_name(nid::Nid::X9_62_PRIME256V1)?; 117 | if bytes.len() != 65 || bytes[0] != 4 { 118 | // It's not a properly tagged key. 119 | return Err(error::VapidErrorKind::PublicKey.into()); 120 | } 121 | let point = ec::EcPoint::from_bytes(&group, &bytes, &mut ctx)?; 122 | Ok(ec::EcKey::from_public_key(&group, &point)?) 123 | } 124 | } 125 | 126 | /// The elements of the Authentication. 127 | #[derive(Debug)] 128 | struct AuthElements { 129 | /// the unjoined JWT components 130 | t: Vec, 131 | /// the public verification key 132 | k: String, 133 | } 134 | 135 | /// Parse the Authorization Header for useful things. 136 | fn parse_auth_token(auth_token: &str) -> Result { 137 | let mut parts: Vec<&str> = auth_token.split(' ').collect(); 138 | let mut schema = parts.remove(0).to_lowercase(); 139 | // Ignore the first token if it's the header line. 140 | if schema == "authorization:" { 141 | schema = parts.remove(0).to_lowercase(); 142 | } 143 | let mut reply: AuthElements = AuthElements { 144 | t: Vec::new(), 145 | k: String::new(), 146 | }; 147 | match schema.to_lowercase().as_ref() { 148 | "vapid" => { 149 | for kvi in parts[0].splitn(2, ',') { 150 | let kv: Vec = kvi.splitn(2, '=').map(String::from).collect(); 151 | match kv[0].to_lowercase().as_ref() { 152 | "t" => { 153 | let ts: Vec = kv[1].split('.').map(String::from).collect(); 154 | if ts.len() != 3 { 155 | return Err("Invalid t token specified".into()); 156 | } 157 | let ttoken = format!("{}.{}", ts[0], ts[1]); 158 | reply.t = vec![ttoken, ts[2].clone()]; 159 | } 160 | "k" => reply.k = kv[1].clone(), 161 | _ => {} 162 | } 163 | } 164 | } 165 | "webpush" => { 166 | reply.t = parts[0].split('.').map(String::from).collect(); 167 | } 168 | _ => return Err(format!("Unknown schema type: {}", parts[0])), 169 | }; 170 | Ok(reply) 171 | } 172 | 173 | // Preferred schema 174 | static SCHEMA: &str = "vapid"; 175 | 176 | fn to_secs(t: SystemTime) -> u64 { 177 | t.duration_since(SystemTime::UNIX_EPOCH) 178 | .unwrap_or_default() 179 | .as_secs() 180 | } 181 | 182 | /// Convert the HashMap containing the claims into an Authorization header. 183 | /// `key` must be generated or initialized before this is used. See `Key::from_pem()` or 184 | /// `Key::generate()`. 185 | pub fn sign( 186 | key: Key, 187 | claims: &mut HashMap, 188 | ) -> error::VapidResult { 189 | // this is the common, static header for all VAPID JWT objects. 190 | let prefix: String = "{\"typ\":\"JWT\",\"alg\":\"ES256\"}".into(); 191 | 192 | // Check the claims 193 | match claims.get("sub") { 194 | Some(sub) => { 195 | if !sub.as_str().unwrap().starts_with("mailto") { 196 | return Err(error::VapidErrorKind::Protocol( 197 | "'sub' not a valid HTML reference".to_owned(), 198 | ) 199 | .into()); 200 | } 201 | } 202 | None => { 203 | return Err(error::VapidErrorKind::Protocol("'sub' not found".to_owned()).into()); 204 | } 205 | } 206 | let today = SystemTime::now(); 207 | let tomorrow = today + time::Duration::hours(24); 208 | claims 209 | .entry(String::from("exp")) 210 | .or_insert_with(|| serde_json::Value::from(to_secs(tomorrow))); 211 | match claims.get("exp") { 212 | Some(exp) => { 213 | let exp_val = exp.as_i64().unwrap(); 214 | if (exp_val as u64) < to_secs(today) { 215 | return Err( 216 | error::VapidErrorKind::Protocol(r#""exp" already expired"#.to_owned()).into(), 217 | ); 218 | } 219 | if (exp_val as u64) > to_secs(tomorrow) { 220 | return Err(error::VapidErrorKind::Protocol( 221 | r#""exp" set too far ahead"#.to_owned(), 222 | ) 223 | .into()); 224 | } 225 | } 226 | None => { 227 | // We already do an insertion on empty, so this should never trigger. 228 | return Err(error::VapidErrorKind::Protocol( 229 | r#""exp" failed to initialize"#.to_owned(), 230 | ) 231 | .into()); 232 | } 233 | } 234 | 235 | let json: String = serde_json::to_string(&claims)?; 236 | let content = format!( 237 | "{}.{}", 238 | base64::encode_config(&prefix, base64::URL_SAFE_NO_PAD), 239 | base64::encode_config(&json, base64::URL_SAFE_NO_PAD) 240 | ); 241 | let auth_k = key.to_public_raw(); 242 | let pub_key = PKey::from_ec_key(key.key)?; 243 | 244 | let mut signer = match Signer::new(MessageDigest::sha256(), &pub_key) { 245 | Ok(t) => t, 246 | Err(err) => { 247 | return Err(error::VapidErrorKind::Protocol(format!( 248 | "Could not sign the claims: {:?}", 249 | err 250 | )) 251 | .into()); 252 | } 253 | }; 254 | signer 255 | .update(&content.clone().into_bytes()) 256 | .expect("Could not encode data for signature"); 257 | let signature = signer.sign_to_vec().expect("Could not finalize signature"); 258 | 259 | // Decode signature BER to r,s pair 260 | let r_off: usize = 3; 261 | // r_len must be > 33. Not checking here because if this ever breaks, we have LOTS of other 262 | // problems. 263 | let r_len = signature[r_off] as usize; 264 | // calculate the offsets for the byte array data we want. 265 | let s_off: usize = r_off + r_len + 2; 266 | let s_len = signature[s_off] as usize; 267 | let mut r_val = &signature[(r_off + 1)..(r_off + 1 + r_len)]; 268 | let mut s_val = &signature[(s_off + 1)..(s_off + 1 + s_len)]; 269 | // Strip the leading 0 if it's present. 270 | if r_len == 33 && r_val[0] == 0 { 271 | r_val = &r_val[1..]; 272 | } 273 | if s_len == 33 && s_val[0] == 0 { 274 | s_val = &s_val[1..]; 275 | } 276 | // we now have the r and s byte arrays. Build the raw RS we need for the signature 277 | // println!("r_val: ({}){:?}\ns_val: ({}){:?} ", r_val.len(), r_val, s_val.len(), s_val); 278 | let mut sigval: Vec = Vec::with_capacity(64); 279 | sigval.extend(r_val); 280 | sigval.extend(s_val); 281 | 282 | let auth_t = format!( 283 | "{}.{}", 284 | content, 285 | base64::encode_config( 286 | unsafe { &String::from_utf8_unchecked(sigval) }, 287 | base64::URL_SAFE_NO_PAD, 288 | ) 289 | ); 290 | 291 | Ok(format!( 292 | "Authorization: {} t={},k={}", 293 | SCHEMA, auth_t, auth_k 294 | )) 295 | } 296 | 297 | /// Verify that the auth token string matches for the verification token string 298 | pub fn verify(auth_token: String) -> Result, String> { 299 | let auth_token = parse_auth_token(&auth_token).expect("Authorization header is invalid."); 300 | let pub_ec_key = 301 | Key::from_public_raw(auth_token.k).expect("'k' token is not a valid public key"); 302 | let pub_key = &match PKey::from_ec_key(pub_ec_key) { 303 | Ok(key) => key, 304 | Err(err) => return Err(format!("Public Key Generation error: {:?}", err)), 305 | }; 306 | let mut verifier = match Verifier::new(MessageDigest::sha256(), pub_key) { 307 | Ok(verifier) => verifier, 308 | Err(err) => return Err(format!("Verifier failed to initialize: {:?}", err)), 309 | }; 310 | 311 | let data = &auth_token.t[0].clone().into_bytes(); 312 | let verif_sig = base64::decode_config( 313 | &auth_token.t[1].clone().into_bytes(), 314 | base64::URL_SAFE_NO_PAD, 315 | ) 316 | .expect("Signature failed to decode from base64"); 317 | verifier 318 | .update(data) 319 | .expect("Data failed to load into verifier"); 320 | 321 | // Extract the values from the combined raw key. 322 | let mut r_val = Vec::with_capacity(32); 323 | let mut s_val = Vec::with_capacity(32); 324 | r_val.extend(verif_sig[0..32].iter()); 325 | s_val.extend(verif_sig[32..].iter()); 326 | 327 | /* Compose the sequence DER by hand, because the current rust libraries lack this. */ 328 | // write r & s as asn1 329 | // Prefix is the "\x02" + the length. We can cheat here because we know how long the keys are. 330 | let mut r_asn = vec![2]; 331 | let mut s_asn = vec![2]; 332 | // check if we need to pad for high order byte 333 | if r_val[0] > 127 { 334 | r_asn.extend_from_slice(&[33, 0]) 335 | } else { 336 | r_asn.extend_from_slice(&[32]) 337 | } 338 | r_asn.append(&mut r_val); 339 | if s_val[0] > 127 { 340 | s_asn.extend_from_slice(&[33, 0]) 341 | } else { 342 | s_asn.extend_from_slice(&[32]) 343 | } 344 | s_asn.append(&mut s_val); 345 | 346 | // seq = "\x30" + (len(rs) + len(ss)) + rs + ss 347 | let mut seq: Vec = vec![48]; 348 | seq.append(&mut vec![(r_asn.len() + s_asn.len()) as u8]); 349 | seq.append(&mut r_asn); 350 | seq.append(&mut s_asn); 351 | 352 | match verifier.verify(&seq) { 353 | Ok(true) => { 354 | // Success! Return the decoded claims. 355 | let token = auth_token.t[0].clone(); 356 | let claim_data: Vec<&str> = token.split('.').collect(); 357 | let bytes = base64::decode_config(&claim_data[1], base64::URL_SAFE_NO_PAD) 358 | .expect("Claims were not properly base64 encoded"); 359 | Ok(serde_json::from_str( 360 | &String::from_utf8(bytes) 361 | .expect("Claims included an invalid character and could not be decoded."), 362 | ) 363 | .expect("Claims are not valid JSON")) 364 | } 365 | Ok(false) => Err("Verify failed".to_string()), 366 | Err(err) => Err(format!("Verify failed {:?}", err)), 367 | } 368 | } 369 | 370 | #[cfg(test)] 371 | mod tests { 372 | use super::{Key, *}; 373 | use std::collections::HashMap; 374 | 375 | fn test_claims() -> HashMap { 376 | let reply: HashMap = [ 377 | ( 378 | String::from("sub"), 379 | serde_json::Value::from("mailto:admin@example.com"), 380 | ), 381 | (String::from("exp"), serde_json::Value::from("1463001340")), 382 | ( 383 | String::from("aud"), 384 | serde_json::Value::from("https://push.services.mozilla.com"), 385 | ), 386 | ] 387 | .iter() 388 | .cloned() 389 | .collect(); 390 | reply 391 | } 392 | 393 | #[test] 394 | fn test_sign() { 395 | let key = Key::generate().unwrap(); 396 | let sub_val = serde_json::Value::from(String::from("mailto:mail@example.com")); 397 | 398 | let mut claims: HashMap = HashMap::new(); 399 | claims.insert(String::from("sub"), sub_val.clone()); 400 | let result = sign(key, &mut claims).unwrap(); 401 | let vresult = result.clone(); 402 | 403 | // println!("{}", result); 404 | 405 | assert!(result.starts_with("Authorization: ")); 406 | assert!(result.contains(" vapid ")); 407 | 408 | // tear apart the auth token for the happy bits 409 | let token = result.split(' ').nth(2).unwrap(); 410 | let sub_parts: Vec<&str> = token.split(',').collect(); 411 | let mut auth_parts: HashMap = HashMap::new(); 412 | for kvi in &sub_parts { 413 | let kv: Vec = kvi.splitn(2, '=').map(String::from).collect(); 414 | auth_parts.insert(kv[0].clone(), kv[1].clone()); 415 | } 416 | assert!(auth_parts.contains_key("t")); 417 | assert!(auth_parts.contains_key("k")); 418 | 419 | // now tear apart the token 420 | let token: Vec<&str> = auth_parts.get("t").unwrap().split('.').collect(); 421 | assert_eq!(token.len(), 3); 422 | 423 | let content = 424 | String::from_utf8(base64::decode_config(token[0], base64::URL_SAFE_NO_PAD).unwrap()) 425 | .unwrap(); 426 | let items: HashMap = serde_json::from_str(&content).unwrap(); 427 | assert!(items.contains_key("typ")); 428 | assert!(items.contains_key("alg")); 429 | 430 | let content: String = 431 | String::from_utf8(base64::decode_config(token[1], base64::URL_SAFE_NO_PAD).unwrap()) 432 | .unwrap(); 433 | let items: HashMap = serde_json::from_str(&content).unwrap(); 434 | 435 | assert!(items.contains_key("exp")); 436 | assert!(items.contains_key("sub")); 437 | assert!(items.get("sub") == Some(&sub_val)); 438 | 439 | // And verify that the signature works. 440 | // we do integration verify in `test_verify` 441 | verify(vresult).expect("Signed claims failed to self verify"); 442 | } 443 | 444 | // TODO: Test fail cases, verification, values 445 | 446 | #[test] 447 | fn test_sign_bad_sub() { 448 | let key = Key::generate().unwrap(); 449 | let mut claims: HashMap = HashMap::new(); 450 | claims.insert( 451 | "sub".into(), 452 | serde_json::Value::from(String::from("invalid")), 453 | ); 454 | match sign(key, &mut claims) { 455 | Ok(_) => panic!("Failed to reject invalid sub"), 456 | Err(err) => { 457 | // not sure how to capture quoted elements in a string 458 | // e.g. errstr.contains("\"sub\" not a valid HTML") fails. 459 | let errstr = format!("{:?}", err); 460 | assert!(errstr.contains("not a valid HTML reference")); 461 | } 462 | } 463 | } 464 | 465 | #[test] 466 | fn test_sign_no_sub() { 467 | let key = Key::generate().unwrap(); 468 | let mut claims: HashMap = HashMap::new(); 469 | claims.insert( 470 | "blah".into(), 471 | serde_json::Value::from(String::from("mailto:a@b.c")), 472 | ); 473 | match sign(key, &mut claims) { 474 | Ok(_) => panic!("Failed to reject missing sub"), 475 | Err(err) => { 476 | let errstr = format!("{:?}", err); 477 | assert!(errstr.contains(" not found")); 478 | } 479 | } 480 | } 481 | 482 | #[test] 483 | fn test_verify_integration() { 484 | // Integration test with externally generated Authorization header. 485 | let test_header = [ 486 | "Authorization: vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwcz\ 487 | ovL3B1c2guc2VydmljZXMubW96aWxsYS5jb20iLCJleHAiOiIxNDYzMDAxMzQwIiwic3ViIjoibWFp\ 488 | bHRvOmFkbWluQGV4YW1wbGUuY29tIn0.4ZiULZaqZ8_7Cf2UYu7KO3eGaqZL5d4RZ6pwBvR0rcmTho\ 489 | 4WryVuZLfN-iMsHJ6Oc-4hkEZsMj8_32sXYSvTyg,k=BPD3F0hvy3Df69tjqRBN0ad08WH2nfaaxnp\ 490 | kuIO6BV9Pa7p8xA8GauX0R_S-D-k82kcTNsCiJ6ML-zJisBpyybs", 491 | ] 492 | .join(""); 493 | assert!(test_claims() == verify(test_header).unwrap()) 494 | } 495 | 496 | //TODO: Add key input/output tests here. 497 | } 498 | --------------------------------------------------------------------------------