Last active
June 8, 2020 08:12
-
-
Save theorm/ac4e6b592585ca16e15ab9b6937c29b5 to your computer and use it in GitHub Desktop.
Validate web push JWT tokens with VAPID
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const asn1 = require('asn1.js'); | |
const crypto = require('crypto'); | |
const libJwt = require('jsonwebtoken'); | |
const urlBase64 = require('urlsafe-base64'); | |
/** | |
* Code below is taken from Mozilla IoT gateway implementation: | |
* https://github.com/mozilla-iot/gateway/blob/c0d902829a410a5ec4feb5379ac04de0161552f1/src/ec-crypto.ts#L29 | |
*/ | |
/** | |
* This curve goes by different names in different standards. | |
* | |
* These are all equivilent for our uses: | |
* | |
* prime256v1 = ES256 (JWT) = secp256r1 (rfc5480) = P256 (NIST). | |
*/ | |
const CURVE = 'prime256v1'; | |
// https://tools.ietf.org/html/rfc5915#section-3 | |
const ECPrivateKeyASN = asn1.define('ECPrivateKey', function() { | |
this.seq().obj( | |
this.key('version').int(), | |
this.key('privateKey').octstr(), | |
this.key('parameters').explicit(0).objid().optional(), | |
this.key('publicKey').explicit(1).bitstr().optional() | |
); | |
}); | |
// https://tools.ietf.org/html/rfc3280#section-4.1 | |
const SubjectPublicKeyInfoASN = asn1.define('SubjectPublicKeyInfo', function() { | |
this.seq().obj( | |
this.key('algorithm').seq().obj( | |
this.key('id').objid(), | |
this.key('namedCurve').objid() | |
), | |
this.key('pub').bitstr() | |
); | |
}); | |
// Chosen because it is _must_ implement. | |
// https://tools.ietf.org/html/rfc5480#section-2.1.1 | |
const UNRESTRICTED_ALGORITHM_ID = [1, 2, 840, 10045, 2, 1]; | |
// https://tools.ietf.org/html/rfc5480#section-2.1.1.1 (secp256r1) | |
const SECP256R1_CURVE = [1, 2, 840, 10045, 3, 1, 7]; | |
/** | |
* Generate a public/private key pair. | |
* | |
* The returned keys are formatted in PEM for use with openssl (crypto). | |
* | |
* @return {Object} .public in PEM. .prviate in PEM. | |
*/ | |
function toPem(publicKey, privateKey) { | |
const key = crypto.createECDH(CURVE); | |
key.generateKeys(); | |
const priv = ECPrivateKeyASN.encode({ | |
version: 1, | |
privateKey: privateKey, | |
parameters: SECP256R1_CURVE | |
}, 'pem', { | |
// https://tools.ietf.org/html/rfc5915#section-4 | |
label: 'EC PRIVATE KEY' | |
}); | |
const pub = SubjectPublicKeyInfoASN.encode({ | |
pub: { | |
unused: 0, | |
data: publicKey | |
}, | |
algorithm: { | |
id: UNRESTRICTED_ALGORITHM_ID, | |
namedCurve: SECP256R1_CURVE | |
} | |
}, 'pem', { | |
label: 'PUBLIC KEY' | |
}); | |
return { public: pub, private: priv }; | |
} | |
function verifyJwt(token, publicKeyAsBase64) { | |
return new Promise((resolve, reject) => { | |
try { | |
const pubKey = toPem(urlBase64.decode(publicKeyAsBase64), urlBase64.decode('')).public; | |
libJwt.verify(token, pubKey, (e, r) => { | |
if (e) { | |
reject(e); | |
} else { | |
resolve(r); | |
} | |
}); | |
} catch (e) { | |
reject(e); | |
} | |
}); | |
} | |
const publicKey = 'BHGS2M5s_HkY_ByoEbvZabEozLOb6xrnaPLoxj5dib8uU3l9rsyG93y3P7hI_s2RglkAiIazQMOzu8_awyz61p8'; | |
const tokens = { | |
nodeJsValid: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTUxNjkyMTE2OSwic3ViIjoibWFpbHRvOnNlcnZpY2UucHJvdmlkZXJzQG90aGVybGV2ZWxzLmNvbSJ9.QuigrT14K7mNmV3SF_lut_DM_PVIwedFhlc1gpJFv5tttJ4KMSDr-mwOZYaKvYSULXAW-oTRLnp5ANDFNpTf5Q', | |
java1Invalid: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTUxNjkyMTQ2MSwic3ViIjoibWFpbHRvOm1pY2hhZWwuaGVycml0eUBvdGhlcmxldmVscy5jb20ifQ.dZrenPBnfEQ6Lxn05y11WStQD4pYehS1YOA5Aa3KM78yiCW99IYDgfeUCBIDT-u9GLs6RhPMmbY3ZepCHQVAdg', | |
java2Invalid: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsInN1YiI6Im1haWx0bzpzZXJ2aWNlLnByb3ZpZGVyc0BvdGhlcmxldmVscy5jb20ifQ.QqHjEuUx-FdXBxyXghnkuzsfTTotqjiVz055rG8PeNBzR-3kH-_onH9hxiMd0VonxjntA-pATLfxNZudX8VaEg', | |
java3Valid: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTUxNjk0MDA4Miwic3ViIjoibWFpbHRvOnNlcnZpY2UucHJvdmlkZXJzQG90aGVybGV2ZWxzLmNvbSJ9.1PPtGNrNX2ShcPSP_Dgw1UOYx8of_WZEPGlVA7XxrGA8vtc_gcPy-dZw4c9-KewrduFy3YHGckXd8PsAEHVXHA' | |
} | |
Promise.all(Object.entries(tokens).map(kv => { | |
const name = kv[0]; | |
const token = kv[1]; | |
verifyJwt(token, publicKey) | |
.then(r => { | |
console.log('------'); | |
console.log(`Verified "${name}" OK:`, r); | |
}) | |
.catch(e => { | |
console.log('------'); | |
console.error(`Could not verify "${name}": `, e.stack); | |
}); | |
})); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.io.BaseEncoding; | |
import org.bouncycastle.jce.ECNamedCurveTable; | |
import org.bouncycastle.jce.interfaces.ECPrivateKey; | |
import org.bouncycastle.jce.interfaces.ECPublicKey; | |
import org.bouncycastle.jce.provider.BouncyCastleProvider; | |
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; | |
import org.bouncycastle.jce.spec.ECParameterSpec; | |
import org.bouncycastle.jce.spec.ECPrivateKeySpec; | |
import org.bouncycastle.jce.spec.ECPublicKeySpec; | |
import org.bouncycastle.math.ec.ECPoint; | |
import java.math.BigInteger; | |
import java.security.*; | |
import java.security.spec.*; | |
import java.util.Base64; | |
public class KeyUtils { | |
/** | |
* Load the private key from a URL-safe base64 encoded string | |
* | |
* @param encodedPrivateKey | |
* @return | |
* @throws NoSuchProviderException | |
* @throws NoSuchAlgorithmException | |
* @throws InvalidKeySpecException | |
*/ | |
public static PrivateKey loadPrivateKey(String encodedPrivateKey) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException { | |
byte[] decodedPrivateKey = base64Decode(encodedPrivateKey); | |
// prime256v1 is NIST P-256 | |
ECParameterSpec params = ECNamedCurveTable.getParameterSpec("prime256v1"); | |
ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(decodedPrivateKey), params); | |
KeyFactory kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME); | |
return kf.generatePrivate(prvkey); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "vapid-jwt-verifier", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"dependencies": { | |
"asn1.js": "^5.0.0", | |
"jsonwebtoken": "^8.1.1", | |
"urlsafe-base64": "^1.0.0" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment