使用Node.js创建Safari推送通知签名

问题描述

我试图按照Node.js中的here实现Safari推送通知,以在Google Cloud Function中运行。

我正在尝试使用forge创建分离的PKCS#7签名,但是我在日志记录端点上总是遇到"Signature verification of push package failed"错误。我尝试将DER和PEM格式的signature编码都没有成功。根据苹果公司的PHP示例,他们需要DER。我也尝试使用safari push notifications package失败。

代码如下:

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;
    }
});

下载并解压缩软件包时,我运行以下命令:

openssl smime -verify -in signature -content manifest.json -inform der -noverify

它返回:Verification successful

关于我要去哪里的任何建议吗?

解决方法

在测试完所有内容后,使用相同的签名方法,我做到了。没有更多的"Signature verification of push package failed"

因为我在本地检查时也得到了“有效签名”,所以我开始在别处寻找根本原因(而不是关注节点伪造代码)。

我认为有些事情很重要(我在进行了一系列更改后尝试过,所以我不确定哪一个是解决方案):

1.首先,检查网络推送 ID。 确保 websitePushID 上的 website.json 与您在 Apple 为签名创建创建 Web Push Certificate 时键入的完全一致。我是从 Web 本身发出的 REST 请求中获取它的,但我完全忘记了这一点,因此在来自 Web 的调用中,我使用的变体与用于证书的变体不同。 (仔细检查下面的 javacript 代码):

window.safari.pushNotification.requestPermission(
            'https://...',WEB_PUSH_ID,<--- THIS must match p12 cert web id. The Website Push ID.
            {},checkRemotePermission         // The callback function.
        );

此外,website.json 本身:

const websiteJson = { 
    websiteName: "...",websitePushID: WEB_PUSH_ID,// <--- THIS must match p12 cert web id. The Website Push ID.
    allowedDomains: ["..."],urlFormatString: "...",authenticationToken: "...",webServiceURL: "...",};

2.放置适当的图标资源 可能不是原因,但出于测试目的,我使用了相同的图标,重命名了 6 次,没有缩放。我刚刚为每种尺寸创建了适当的资产。

3.最后,我用于签名生成的代码段 以防万一。 (请注意,我删除了所有额外的 ``,但可能不是这样,因为我之前得到了相同的结果。

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.还有一件事。 可能不相关,因为您似乎可以毫无问题地提取包、证书和密钥;但因为我挣扎,得到空洞和未定义的,我会离开这里我是怎么做的

    // 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

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...