使用VAPID密钥发送Web Push通知时遇私钥错误求助
Web Push通知开发错误排查:
openssl_sign()私钥格式问题 我按照以下步骤开发Web Push通知:
- 生成并存储VAPID密钥;
- 使用Push API存储endpoint、p256dh和auth参数;
- 通过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);
额外问题修正
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', ];请求体格式错误:Web Push协议不需要将
endpoint、p256dh、auth放入请求体,直接发送payload JSON即可:$options = [ 'http' => [ 'header' => implode("\r\n", $headers), 'method' => 'POST', 'content' => $payloadJson, // 直接发送payload ], ];
内容的提问来源于stack exchange,提问作者Sikki
相关产品推荐
相关产品推荐

