You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用VAPID密钥发送Web Push通知时遇私钥错误求助

Web Push通知开发错误排查:openssl_sign()私钥格式问题

我按照以下步骤开发Web Push通知:

  1. 生成并存储VAPID密钥;
  2. 使用Push API存储endpoint、p256dh和auth参数;
  3. 通过Web Push协议发送推送通知。

但在第三步执行时出现警告:openssl_sign(): Supplied key param cannot be coerced into a private key。以下是完整代码及错误排查方案:


服务端PHP生成VAPID密钥并存储

// Generate VAPID keys
$private_key = openssl_pkey_new([
    'private_key_type' => OPENSSL_KEYTYPE_EC,
    'curve_name' => 'prime256v1',
]);

$details = openssl_pkey_get_details($private_key);
$private_key_raw = $details['ec']['d'];
$public_key_raw = $details['ec']['x'] . $details['ec']['y'];
$auth_token = base64_encode(openssl_random_pseudo_bytes(16));

$vapid = [
    'private_key' => rtrim(strtr(base64_encode($private_key_raw), '+/', '-_'), '='),
    'public_key' => rtrim(strtr(base64_encode($public_key_raw), '+/', '-_'), '='),
    'auth_token' => $auth_token,
];

echo json_encode($vapid);

使用公钥生成applicationServerKey

$publicKey = 'SzRJTTxfRvvoIfYJye-Oj-xJ-eDxHjIBhPLxILieNbZ86KjRE_EIvdjdKDmUH9RLwgmkITs-v_z_6J44aP1TtA';
// Base64-decode the public key
$public_key_bytes = base64_decode($publicKey);

// Check that the public key has the correct format
if (strlen($public_key_bytes) != 65 || ord($public_key_bytes[0]) != 4) {
    // The public key has an incorrect format
    // Handle the error here
}

// Extract the x and y coordinates of the point
$x = substr($public_key_bytes, 1, 32);
$y = substr($public_key_bytes, 33, 32);

// Pack the bytes of the public key in the correct order
$application_server_key = "\x04" . $x . $y;
echo base64_encode($application_server_key);

客户端使用base64格式的applicationServerKey

let b64ASK = 'BDRJTTxfRvvoIfYJyeOjxJeDxHjIBhPLxILieNbZ86KjREEIvdjdKDmUH9RLwgmkITsvz6J44aP1TtAauthtokc=';
let asKey = encodeToUint8Array(b64ASK);
pushManager.subscribe({applicationServerKey:asKey,userVisibleOnly:true})
.then(subscription=>{console.log(subscription.toJSON())})

服务端存储参数并发送通知(send-notification.php)

$publicKey = 'SzRJTTxfRvvoIfYJye-Oj-xJ-eDxHjIBhPLxILieNbZ86KjRE_EIvdjdKDmUH9RLwgmkITs-v_z_6J44aP1TtA';
$privateKey = '-----BEGIN EC PRIVATE KEY-----\n    Lnp9eUjWE7o8oqneZPzOW8nbz7hTVosE25sJm47Arrg=\n    -----END EC PRIVATE KEY-----';
$endpoint = "https://fcm.googleapis.com/fcm/send/cI0XqQ4quFM:APA91bExZFUuSZ9lgTDJQqmrHJpV-w5pIVnvaiODI9WIeER-K0Vg0U5P8wfTslRF5KdTlCmF9_Tp7bpAohKLxLvQCuS2Cy6ZG2BpVKO4f0wLWrfU-mGD6GCMCVUYLna3uwDLR6NqZxNi";
$auth = "X6syP0cUxjDjMAcUunH3FA";
$p256dh = "BIUuCWdoJykH6u3vERcfZe8gxEx1ajaFTPGnM4cWdYv-Hp-qRgt5GShAtbFYRr5my8hH66uIJEiHf22XW_i_Bps";
// Set the payload for the notification
$payload = [
    'title' => 'push-message-test',
    'body' => 'This is a test notification using VAPID.',
    'icon' => 'assets/icons/favicon-32x32.png',
];

// Encode the payload as a JSON string
$payloadJson = json_encode($payload);

// Generate the JWT header
$header = [
    'alg' => 'ES256',
    'typ' => 'JWT',
];
$headerJson = json_encode($header);
$headerBase64Url = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($headerJson));

// Generate the JWT claim
$now = time();
$exp = $now + (12 * 60 * 60); // 12 hours in the future
$claim = [
    'aud' => $endpoint,
    'exp' => $exp,
    'sub' => 'https://example.com',
];
$claimJson = json_encode($claim);
$claimBase64Url = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($claimJson));

// Generate the JWT signature
$signingString = $headerBase64Url . '.' . $claimBase64Url;
$signature = '';
$privateKeyResource = openssl_pkey_get_private($privateKey);
openssl_sign($signingString, $signature, $privateKeyResource, 'sha256');
// Encode the signature as base64url
$signatureBase64Url = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

// Combine the JWT components into a string
$jwt = $headerBase64Url . '.' . $claimBase64Url . '.' . $signatureBase64Url;

// Send the notification using the Web Push protocol
$headers = [
    'Authorization: Bearer ' . $jwt,
    'Crypto-Key: p256ecdsa=' . $publicKey,
    'Content-Length: ' . strlen($payloadJson),
    'Content-Type: application/json',
];
$data = [
    'endpoint' => $endpoint,
    'publicKey' => $p256dh,
    'authToken' => $auth,
    'payload' => $payloadJson,
];
$options = [
    'http' => [
        'header' => implode("\r\n", $headers),
        'method' => 'POST',
        'content' => json_encode($data),
    ],
];
$context = stream_context_create($options);
$result = file_get_contents($endpoint, false, $context);

// Handle the response
if ($result === false) {
    // Error handling
    echo 'failed to send push';
} else {
    // Success handling
    echo 'succesfully sent push using push protocol';
}

错误原因及修复方案

核心问题:私钥格式不合法

你生成私钥时提取的是原始EC私钥字节($details['ec']['d']),并转成base64url存储,但发送时手动拼接了PEM格式的字符串。OpenSSL无法识别这种伪PEM密钥——原始字节缺少PEM格式必需的ASN.1编码结构。

修复方案1:生成时直接存储PEM格式私钥

修改密钥生成代码,直接导出标准PEM格式私钥:

// Generate VAPID keys
$private_key = openssl_pkey_new([
    'private_key_type' => OPENSSL_KEYTYPE_EC,
    'curve_name' => 'prime256v1',
]);

// 直接导出PEM格式私钥
openssl_pkey_export($private_key, $private_key_pem);

$details = openssl_pkey_get_details($private_key);
$public_key_raw = $details['ec']['x'] . $details['ec']['y'];
$auth_token = base64_encode(openssl_random_pseudo_bytes(16));

$vapid = [
    'private_key' => $private_key_pem, // 存储标准PEM格式私钥
    'public_key' => rtrim(strtr(base64_encode($public_key_raw), '+/', '-_'), '='),
    'auth_token' => $auth_token,
];

echo json_encode($vapid);

修复方案2:将已存储的原始私钥转换为PEM格式

如果已经用原始字节方式存储了私钥,可通过ASN.1编码转换为合法PEM:

// 从存储中取出的base64url格式私钥
$stored_private_key = 'Lnp9eUjWE7o8oqneZPzOW8nbz7hTVosE25sJm47Arrg';
// 还原为原始字节
$private_key_raw = base64_decode(strtr($stored_private_key, '-_', '+/'));

// 构造ASN.1编码结构
$asn1 = pack('H*', '30770201010420') . $private_key_raw . pack('H*', 'a00a06082a8648ce3d030107');
// 转换为PEM格式
$private_key_pem = "-----BEGIN EC PRIVATE KEY-----\n" . chunk_split(base64_encode($asn1), 64) . "-----END EC PRIVATE KEY-----";

// 现在可正常使用该密钥
$privateKeyResource = openssl_pkey_get_private($private_key_pem);

额外问题修正

  1. JWT的aud字段错误:aud应为endpoint的域名部分,而非完整URL。例如FCM的aud是https://fcm.googleapis.com:

    $url_parts = parse_url($endpoint);
    $claim = [
        'aud' => $url_parts['scheme'] . '://' . $url_parts['host'],
        'exp' => $exp,
        'sub' => 'https://example.com',
    ];
    
  2. 请求体格式错误:Web Push协议不需要将endpoint、p256dh、auth放入请求体,直接发送payload JSON即可:

    $options = [
        'http' => [
            'header' => implode("\r\n", $headers),
            'method' => 'POST',
            'content' => $payloadJson, // 直接发送payload
        ],
    ];
    

内容的提问来源于stack exchange,提问作者Sikki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 20:01:33