I am trying to implement Safari Push Notifications as described here in Node.js to run in a Google Cloud Function.
I am trying to use forge to create the detached PKCS#7 signature, but I always get a "Signature verification of push package failed"
error on my logging endpoint. I have tried encoding the signature
in both DER and PEM formats with no success. Based on Apple's PHP example, they want DER. I have also tried using the safari push notifications
package with no success.
Here is the code:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import fs from "fs";
import path from "path";
import express from "express";
import crypto from "crypto";
import forge from "node-forge";
import archiver from "archiver";
const app = express();
const iconFiles = [
"icon_16x16.png",
"icon_16x16@2x.png",
"icon_32x32.png",
"icon_32x32@2x.png",
"icon_128x128.png",
"icon_128x128@2x.png",
];
const websiteJson = {
websiteName: "...",
websitePushID: "web.<...>",
allowedDomains: ["..."],
urlFormatString: "...",
authenticationToken: "...",
webServiceURL: "...",
};
const p12Asn1 = forge.asn1.fromDer(fs.readFileSync("./certs/apple_push.p12", 'binary'));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, functions.config().safari.keypassword);
const certBags = p12.getBags({bagType: forge.pki.oids.certBag});
const certBag = certBags[forge.pki.oids.certBag];
const cert = certBag[0].cert;
const keyBags = p12.getBags({bagType: forge.pki.oids.pkcs8ShroudedKeyBag});
const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
const key = keyBag[0].key;
const intermediate = forge.pki.certificateFromPem(fs.readFileSync("./certs/intermediate.pem", "utf8"));
app.post("/:version/pushPackages/:websitePushId", async (req, res) => {
if (!cert) {
console.log("cert is null");
res.sendStatus(500);
return;
}
if (!key) {
console.log("key is null");
res.sendStatus(500);
return;
}
const iconSourceDir = "...";
res.attachment("pushpackage.zip");
const archive = archiver("zip");
archive.on("error", function (err) {
res.status(500).send({ error: err.message });
return;
});
archive.on("warning", function (err) {
if (err.code === "ENOENT") {
console.log(`Archive warning ${err}`);
} else {
throw err;
}
});
archive.on("end", function () {
console.log("Archive wrote %d bytes", archive.pointer());
});
archive.pipe(res);
archive.directory(iconSourceDir, "icon.iconset");
const manifest: {
[key: string]: { hashType: string; hashValue: string };
} = {};
const readPromises: Promise<void>[] = [];
iconFiles.forEach((i) =>
readPromises.push(
new Promise((resolve, reject) => {
const hash = crypto.createHash("sha512");
const readStream = fs.createReadStream(
path.join(iconSourceDir, i),
{ encoding: "utf8" }
);
readStream.on("data", (chunk) => {
hash.update(chunk);
});
readStream.on("end", () => {
const digest = hash.digest("hex");
manifest[`icon.iconset/${i}`] = {
hashType: "sha512",
hashValue: `${digest}`,
};
resolve();
});
readStream.on("error", (err) => {
console.log(`Error on readStream for ${i}; ${err}`);
reject();
});
})
)
);
try {
await Promise.all(readPromises);
} catch (error) {
console.log(`Error writing files; ${error}`);
res.sendStatus(500);
return;
}
const webJSON = {
...websiteJson,
...{ authenticationToken: "..." },
};
const webHash = crypto.createHash("sha512");
const webJSONString = JSON.stringify(webJSON);
webHash.update(webJSONString);
manifest["website.json"] = {
hashType: "sha512",
hashValue: `${webHash.digest("hex")}`,
};
const manifestJSONString = JSON.stringify(manifest);
archive.append(webJSONString, { name: "website.json" });
archive.append(manifestJSONString, { name: "manifest.json" });
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer(manifestJSONString, "utf8");
p7.addCertificate(cert);
p7.addCertificate(intermediate);
p7.addSigner({
// @ts-ignore
key,
certificate: cert,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data
}, {
type: forge.pki.oids.messageDigest
}, {
type: forge.pki.oids.signingTime,
value: new Date().toString()
}]
});
p7.sign({ detached: true });
const pem = forge.pkcs7.messageToPem(p7);
archive.append(Buffer.from(pem, 'binary'), { name: "signature" });
// Have also tried this:
// archive.append(forge.asn1.toDer(p7.toAsn1()).getBytes(), { name: "signature" });
try {
await archive.finalize();
} catch (error) {
console.log(`Error on archive.finalize(); ${error}`);
res.sendStatus(500);
return;
}
});
When I download and unzip my package, I run the following command:
openssl smime -verify -in signature -content manifest.json -inform der -noverify
And it returns: Verification successful
Any suggestions on where I am going wrong?
After testing everything, using the same approach to signing, I did it. No more "Signature verification of push package failed"
.
Since I was also getting a "Valid Signature" when checking locally, I started looking elsewhere for the root cause (instead of focusing on the node-forge code).
Some things I think it mattered (I tried after making a bunch of changes, so I'm not sure which one is the solution):
1. First, check the Web Push Id. Make sure the websitePushID
on your website.json
matches the exact one you typed when creating your Web Push Certificate
at Apple for the signature creation. I was grabbing it from the REST request made from the web itself, and I totally forgot that, so on the call from the web I was using a different variation than the one used for the certificate. (double check the javacript code below):
window.safari.pushNotification.requestPermission(
'https://...',
WEB_PUSH_ID, <--- THIS must match p12 cert web id. The Website Push ID.
{},
checkRemotePermission // The callback function.
);
Also, the website.json
itself:
const websiteJson = {
websiteName: "...",
websitePushID: WEB_PUSH_ID, // <--- THIS must match p12 cert web id. The Website Push ID.
allowedDomains: ["..."],
urlFormatString: "...",
authenticationToken: "...",
webServiceURL: "...",
};
2. Place proper icon assets Probably not the cause, but for testing purposes, I was using the same icon, renamed 6 times, without scaling. I just created proper assets for each size.
3. Finally, the snippet I use for signature generation Just in case. (Note that I removed all the extra ``, but that might not be it, because I was getting same results before.
function signature(manifestData, certOrCertPem, privateKeyAssociatedWithCert)
{
//A. load the WWWDC cert, always the same
var intermediateBinnary = fs.readFileSync(Path.resolve('.') + '/AppleWWDRCA.pem', 'utf8')
//console.log('pem wwwdc ', intermediateBinnary);
//B. continue signing
var p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer(manifestData, 'utf8');
p7.addCertificate(certOrCertPem);
p7.addSigner({
key: privateKeyAssociatedWithCert,
certificate: certOrCertPem,
digestAlgorithm: forge.pki.oids.sha256
});
p7.addCertificate(intermediateBinnary);
p7.sign({detached: true});
//console.log('p7: ',p7)
var pem = forge.pkcs7.messageToPem(p7);
console.log('pem: ',pem)
// var lines = pem.split('\n')
// console.log('lines ',lines);
// We need to turn into DER according to Apple (sure there are better ways tho)
var preDer = pem.replace('-----BEGIN PKCS7-----\r\n','');
preDer = preDer.replace('\r\n-----END PKCS7-----','');
//console.log('-+pem: ',preDer)
// var lines = preDer.split('\n')
// console.log('lines ',lines);
return preDer;
}
// I call this signature method from:
...
var contentSignature = signature(contentManifestString, certPem, privatePem);
var bufferFromPem = Buffer.from(contentSignature, 'base64');
...
// Just add the bufferFromPem to a file
4. Just one more thing. Probably not related, since you seem to be extracting bags, cert and keys without problems; but since I struggled, getting empty and undefined ones, I'll leave here how I did it
// Prepare
var p12 = fs.readFileSync(Path.resolve('.') + '/Cert.p12', 'binary');
var p12Asn1 = forge.asn1.fromDer(p12, false);
var p12Parsed = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, 'HERE_PASSWORD');
// extract bags: https://github.com/digitalbazaar/forge/issues/533
const keyData = p12Parsed.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag]
.concat(p12Parsed.getBags({ bagType: forge.pki.oids.keyBag })[forge.pki.oids.keyBag]);
const certBags = p12Parsed.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag];
// convert a Forge private key to an ASN.1 RSAPrivateKey
var rsaPrivateKey = forge.pki.privateKeyToAsn1(keyData[0].key);
// wrap an RSAPrivateKey ASN.1 object in a PKCS#8 ASN.1 PrivateKeyInfo
var privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);
// convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
var privatePem = forge.pki.privateKeyInfoToPem(privateKeyInfo); // <- KEY
// Get cert as well (pem)
var certPem = forge.pki.certificateToPem(certBags[0].cert); // <- CERT
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.