Skip to content

Instantly share code, notes, and snippets.

@theorm
Last active June 8, 2020 08:12
Show Gist options
  • Save theorm/ac4e6b592585ca16e15ab9b6937c29b5 to your computer and use it in GitHub Desktop.
Save theorm/ac4e6b592585ca16e15ab9b6937c29b5 to your computer and use it in GitHub Desktop.
Validate web push JWT tokens with VAPID
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);
});
}));
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);
}
}
{
"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