Browse Source

Appears fixed Crypto-Key header

unsubscribe-mute
Matthew Raymer 1 year ago
parent
commit
2125a0c32b
  1. BIN
      crypto_playpen/header.bin
  2. 1
      crypto_playpen/message.txt
  3. 5
      crypto_playpen/privatekey.pem
  4. 1
      crypto_playpen/public_key.bin
  5. BIN
      crypto_playpen/public_key.der
  6. 4
      crypto_playpen/publickey.pem
  7. BIN
      crypto_playpen/signature.bin
  8. 67
      keys.md
  9. 33
      src/notificationService.ts
  10. 64
      src/vapidService.ts

BIN
crypto_playpen/header.bin

Binary file not shown.

1
crypto_playpen/message.txt

@ -0,0 +1 @@
hello!

5
crypto_playpen/privatekey.pem

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOjRzTX6T5FkhmOscZZdGp1b1PuOgk2p/YoJ7abFaJPPoAoGCCqGSM49
AwEHoUQDQgAEQazvs+7/4y9drkN8RZCB3ZCFVhMZQLtcJmgeY5x9+RXqYE18VHJs
qagywecu9JLckZFFcraOX2hsifyEPQgCYw==
-----END EC PRIVATE KEY-----

1
crypto_playpen/public_key.bin

@ -0,0 +1 @@
A¬ï³îÿã/]®C|E��Ý�…V@»\&hcœ}ùê`M|Trl©¨2Áç.ô’Ü‘‘Er¶Ž_hl‰ü„=c

BIN
crypto_playpen/public_key.der

Binary file not shown.

4
crypto_playpen/publickey.pem

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQazvs+7/4y9drkN8RZCB3ZCFVhMZ
QLtcJmgeY5x9+RXqYE18VHJsqagywecu9JLckZFFcraOX2hsifyEPQgCYw==
-----END PUBLIC KEY-----

BIN
crypto_playpen/signature.bin

Binary file not shown.

67
keys.md

@ -0,0 +1,67 @@
# NOTES on working with Cryptographic Keys
Since the VAPID key pair was created using cyprto.createECDH we could reconstitute our public key
using only the private key:
```
const curveName = 'prime256v1';
const ecdh = crypto.createECDH(curveName);
const privateKeyBuffer = Buffer.from(privateKey, 'base64');
ecdh.setPrivateKey(privateKeyBuffer);
const rawPublicKeyBuffer = ecdh.getPublicKey();
```
Unfortunately, crypto module creates only "raw" keys. And when working with jsonwebtoken.sign method
we must have a PEM or something with ASN metadata. So, we create PEMs using eckeys-util module:
```
const pems = ecKeyUtils.generatePem({curveName, privateKey: ecdh.getPrivateKey(), publicKey: rawPublicKeyBuffer });
console.log("privateKey: ", pems.privateKey);
console.log();
console.log("publicKey: ", pems.publicKey);
const jwtToken = jwt.sign(jwtInfo, pems.privateKey, { algorithm: 'ES256' });
```
I trie here to create my own ASN1 metadata but this seems doomed due to ignorance of what were the required
components:
```
const asn1Header = Buffer.from('3059301306072a8648ce3d020106082a8648ce3d030107034200', 'hex');
const derPublicKeyBuffer = Buffer.concat([asn1Header, rawPublicKeyBuffer]);
const base64DerPublicKey = derPublicKeyBuffer.toString('base64');
console.log("base64DerPublicKey: ", base64DerPublicKey)
```
Such an approach creates a DER key pair. An alternative to that method is:
```
const ders = ecKeyUtils.generateDer({curveName, privateKey: ecdh.getPrivateKey(), publicKey: rawPublicKeyBuffer });
console.log("privateKey: ", ders.privateKey);
console.log("publicKey: ", ders.publicKey);
```
... using eckeys-util again ... but I'm not 100% sure if these have all the necessary ASN1 metadata AND
the DER key will produce this error ...
```
Error: secretOrPrivateKey must be an asymmetric key when using ES256
at module.exports [as sign] (/usr/src/app/node_modules/jsonwebtoken/sign.js:124:22)
```
... when used in the sign method. So, apparently, sign does not like the DER binary format but it is
fine with PEM.
## When sending a notification request to the Mozilla endpoint it does not like the Crypto-Key header:
```
{
"code":400,
"errno":110,
"error":"Bad Request",
"message":"Invalid aes128gcm Crypto-Key header",
"more_info":"http://autopush.readthedocs.io/en/latest/http.html#error-codes"
}
```

33
src/notificationService.ts

@ -1,5 +1,6 @@
import VapidService from './vapidService.js'; import VapidService from './vapidService.js';
import { VapidKeys } from './VapidKeys.js'; import { VapidKeys } from './VapidKeys.js';
import { IncomingMessage } from 'http';
import * as https from 'https'; import * as https from 'https';
import * as http_ece from 'http_ece'; import * as http_ece from 'http_ece';
import crypto from 'crypto'; import crypto from 'crypto';
@ -19,6 +20,9 @@ export interface BrowserSubscription {
}; };
} }
interface MyIncomingMessage extends IncomingMessage {
errno?: number;
}
export class NotificationService { export class NotificationService {
private static instance: NotificationService; private static instance: NotificationService;
@ -39,6 +43,7 @@ export class NotificationService {
await this.pushToEndpoint(subscription, message); await this.pushToEndpoint(subscription, message);
} }
private async pushToEndpoint(subscription: BrowserSubscription, message: Message): Promise<void> { private async pushToEndpoint(subscription: BrowserSubscription, message: Message): Promise<void> {
const payloadString = JSON.stringify(message); const payloadString = JSON.stringify(message);
const payloadBuffer = Buffer.from(payloadString, 'utf-8'); const payloadBuffer = Buffer.from(payloadString, 'utf-8');
@ -55,27 +60,29 @@ export class NotificationService {
method: 'POST', method: 'POST',
hostname: parsedUrl.hostname, hostname: parsedUrl.hostname,
path: parsedUrl.pathname, path: parsedUrl.pathname,
port: parsedUrl.port, port: 443,
headers: { headers: { ...vapidHeaders, 'TTL': '60', 'Content-Encoding': 'aes128gcm', 'Content-Type': 'application/octet-stream', 'Content-Length': encrypted.length },
...vapidHeaders,
'TTL': '60',
'Content-Encoding': 'aes128gcm',
'Content-Type': 'application/octet-stream',
'Content-Length': encrypted.length
},
}; };
console.log(parsedUrl);
console.log(options);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = https.request(options, (res) => { const req = https.request(options, (res: MyIncomingMessage) => {
let body = '';
console.log('Headers:', res.headers);
res.on('data', chunk => { body += chunk; });
res.on('end', () => {
console.log('Body:', body);
console.log(res.statusCode);
if (res.statusCode! >= 200 && res.statusCode! < 300) { if (res.statusCode! >= 200 && res.statusCode! < 300) {
resolve(); resolve();
} else { } else {
reject(new Error(`Failed to send push notification. Status code: ${res.statusCode}`)); reject(new Error(`Failed to send push notification. Status code: ${res.statusCode}, Body: ${body}`));
} }
}); });
});
req.on('error', (error) => { req.on('error', (error) => {
reject(new Error(`Failed to send push notification. Error: ${error.message}`)); reject(new Error(`Failed to send push notification. Error: ${error.message}`));

64
src/vapidService.ts

@ -59,9 +59,23 @@ class VapidService {
} }
private isP256Key(publicKeyBuffer) {
if (publicKeyBuffer.length === 65 && publicKeyBuffer[0] === 0x04) {
return true;
}
if (publicKeyBuffer.length === 33 && (publicKeyBuffer[0] === 0x02 || publicKeyBuffer[0] === 0x03)) {
return true;
}
return false;
}
async createVapidAuthHeader(endpoint: string, expiration: number, subject: string, appKeys: VapidKeys): Promise<{ 'Authorization': string, 'Crypto-Key': string }> { async createVapidAuthHeader(endpoint: string, expiration: number, subject: string, appKeys: VapidKeys): Promise<{ 'Authorization': string, 'Crypto-Key': string }> {
const { publicKey, privateKey } = appKeys; const { publicKey, privateKey } = appKeys;
console.log(publicKey);
const jwtInfo = { const jwtInfo = {
aud: new URL(endpoint).origin, aud: new URL(endpoint).origin,
exp: Math.floor((Date.now() / 1000) + expiration), exp: Math.floor((Date.now() / 1000) + expiration),
@ -72,14 +86,58 @@ class VapidService {
const privateKeyBuffer = Buffer.from(privateKey, 'base64'); const privateKeyBuffer = Buffer.from(privateKey, 'base64');
ecdh.setPrivateKey(privateKeyBuffer); ecdh.setPrivateKey(privateKeyBuffer);
const pems = ecKeyUtils.generatePem({curveName, privateKey: ecdh.getPrivateKey(), publicKey: ecdh.getPublicKey() }); const rawPublicKeyBuffer = ecdh.getPublicKey();
const asn1Header = Buffer.from('3059301306072a8648ce3d020106082a8648ce3d030107034200', 'hex');
const derPublicKeyBuffer = Buffer.concat([asn1Header, rawPublicKeyBuffer]);
const base64DerPublicKey = derPublicKeyBuffer.toString('base64');
console.log("base64DerPublicKey: ", base64DerPublicKey)
const pems = ecKeyUtils.generatePem({curveName, privateKey: ecdh.getPrivateKey(), publicKey: rawPublicKeyBuffer });
console.log("privateKey: ", pems.privateKey);
console.log();
console.log("publicKey: ", pems.publicKey);
const ders = ecKeyUtils.generateDer({curveName, privateKey: ecdh.getPrivateKey(), publicKey: rawPublicKeyBuffer });
console.log("privateKey: ", ders.privateKey);
console.log("publicKey: ", ders.publicKey);
const jwtToken = jwt.sign(jwtInfo, pems.privateKey, { algorithm: 'ES256' }); const jwtToken = jwt.sign(jwtInfo, pems.privateKey, { algorithm: 'ES256' });
return { try {
const decoded = Buffer.from(base64DerPublicKey, 'base64').toString('utf-8');
console.log('Valid Base64 Encoded:', decoded);
console.log('isP256Key:', this.isP256Key(Buffer.from(base64DerPublicKey, 'base64')) );
} catch (error) {
console.log('Invalid Base64 Encoding');
}
try {
const decodedKey = Buffer.from(base64DerPublicKey, 'base64');
console.log('Decoded Key Length:', decodedKey.length);
if (decodedKey.length !== 65) {
console.log('Invalid Key Length');
} else {
console.log('Key Length is Valid');
}
} catch (err) {
console.log('Invalid Base64-URL Encoding');
}
const result = {
'Authorization': `vapid t=${jwtToken}, k=${publicKey}`, 'Authorization': `vapid t=${jwtToken}, k=${publicKey}`,
'Crypto-Key': publicKey 'Crypto-Key': "p256ecdsa="+ders.publicKey.toString('base64')
}; };
console.log(result);
return result;
} }
} }

Loading…
Cancel
Save