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

ASP.NET Core Web Push的JWT授权算法异常问题排查

Web Push VAPID JWT签名(ES256)问题排查与修复

核心问题分析

你的代码存在两个关键错误,直接导致JWT验证失败和401"Unknown auth scheme"错误:

  1. ECDSA签名格式不兼容:.NET的ECDsa.SignData默认返回ASN.1/DER编码的签名,但JWT的ES256算法要求签名是原始r+s拼接格式(64字节:前32字节为r,后32字节为s)。
  2. JWT的exp声明类型错误:你将exp转为字符串,但JWT标准要求exp必须是Unix时间戳的数值类型,而非字符串。

修正后的代码实现

1. 修复JWT Payload的exp类型

将jwt_exp改为数值类型,避免字符串转换:

long jwt_exp = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds() + 12 * 60 * 60;
var jwt2 = new { aud = jwt_aud, exp = jwt_exp, sub = jwt_sub };

2. 添加签名格式转换方法

将DER格式签名转为JWT要求的紧凑r+s格式:

private byte[] ConvertDerSignatureToJwtFormat(byte[] derSignature)
{
    using var ms = new MemoryStream(derSignature);
    using var reader = new BinaryReader(ms);

    if (reader.ReadByte() != 0x30)
        throw new InvalidOperationException("无效的DER签名格式");

    int length = reader.ReadByte();
    if (length == 0x81)
        length = reader.ReadByte();

    if (reader.ReadByte() != 0x02)
        throw new InvalidOperationException("无效的DER签名格式");

    int rLength = reader.ReadByte();
    byte[] r = reader.ReadBytes(rLength);
    if (r.Length < 32)
        r = new byte[32 - r.Length].Concat(r).ToArray();

    if (reader.ReadByte() != 0x02)
        throw new InvalidOperationException("无效的DER签名格式");

    int sLength = reader.ReadByte();
    byte[] s = reader.ReadBytes(sLength);
    if (s.Length < 32)
        s = new byte[32 - s.Length].Concat(s).ToArray();

    byte[] compactSignature = new byte[64];
    Buffer.BlockCopy(r, 0, compactSignature, 0, 32);
    Buffer.BlockCopy(s, 0, compactSignature, 32, 32);
    return compactSignature;
}

3. 修改签名生成逻辑

在签名后转换格式,并确保请求头正确:

using (var ecdsa = ECDsa.Create(ecParameters))
{
    byte[] derSignatureBytes = ecdsa.SignData(unsigned_token, HashAlgorithmName.SHA256);
    // 转换为JWT兼容的紧凑格式
    byte[] jwtSignatureBytes = ConvertDerSignatureToJwtFormat(derSignatureBytes);
    string signature = Base64Url.Base64UrlEncode(jwtSignatureBytes);

    string fullJwt = $"{base64_1}.{base64_2}.{signature}";
    Console.WriteLine("生成的JWT: " + fullJwt);

    // 发送推送请求必须携带以下两个头
    // Authorization: Bearer {fullJwt}
    // Crypto-Key: p256ecdsa={你的VAPID公钥Base64Url编码}
}

4. 完整修正后的Action代码

[ActionName("trigger-push-message")]
[HttpPost]
public async Task<IActionResult> triggerPushMessage()
{
    var first = _context.Subscribers.First();
    
    string jwt_aud = first.Endpoint.Substring(0, 1 + first.Endpoint.IndexOf('/', 8));
    long jwt_exp = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds() + 12 * 60 * 60;
    string jwt_sub = "mailto:emailhere@example.com";

    var jwtHeader = new { typ = "JWT", alg = "ES256" };
    var jwtPayload = new { aud = jwt_aud, exp = jwt_exp, sub = jwt_sub };
    
    string base64Header = Base64Url.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(jwtHeader)));
    string base64Payload = Base64Url.Base64UrlEncode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(jwtPayload)));
    byte[] unsignedToken = Encoding.UTF8.GetBytes($"{base64Header}.{base64Payload}");
    
    string vapidPublicKey = "BNIL96_6318XSC6iGF8pOxThvifLMvN5DHtwHwPKSgki0r6QzOvbxGJ6bg6yPu0fJ9aqml-YXpA0UybWvvjeVwc";
    string vapidPrivateKey = "gOQ-7RPtxyDdJvMMbc0KrCNyDWaZw60Hmf8T-eHTjqw";
    
    byte[] publicKeyBytes = Base64Url.Base64UrlDecode(vapidPublicKey);
    byte[] privateKeyBytes = Base64Url.Base64UrlDecode(vapidPrivateKey);
    byte[] publicKeyX = new byte[32];
    byte[] publicKeyY = new byte[32];
    Array.Copy(publicKeyBytes, 1, publicKeyX, 0, 32);
    Array.Copy(publicKeyBytes, 33, publicKeyY, 0, 32);

    var ecParameters = new ECParameters
    {
        Curve = ECCurve.NamedCurves.nistP256,
        D = privateKeyBytes,
        Q = new ECPoint { X = publicKeyX, Y = publicKeyY }
    };

    using (var ecdsa = ECDsa.Create(ecParameters))
    {
        byte[] derSignature = ecdsa.SignData(unsignedToken, HashAlgorithmName.SHA256);
        byte[] jwtSignature = ConvertDerSignatureToJwtFormat(derSignature);
        string signatureBase64Url = Base64Url.Base64UrlEncode(jwtSignature);

        string fullJwt = $"{base64Header}.{base64Payload}.{signatureBase64Url}";
        Console.WriteLine("JWT: " + fullJwt);

        // 示例:发送Web Push请求
        // using var httpClient = new HttpClient();
        // httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {fullJwt}");
        // httpClient.DefaultRequestHeaders.Add("Crypto-Key", $"p256ecdsa={vapidPublicKey}");
        // var response = await httpClient.PostAsync(first.Endpoint, new StringContent("{}", Encoding.UTF8, "application/json"));
    }

    return NoContent();
}

private byte[] ConvertDerSignatureToJwtFormat(byte[] derSignature)
{
    using var ms = new MemoryStream(derSignature);
    using var reader = new BinaryReader(ms);

    if (reader.ReadByte() != 0x30)
        throw new InvalidOperationException("无效的DER签名格式");

    int length = reader.ReadByte();
    if (length == 0x81)
        length = reader.ReadByte();

    if (reader.ReadByte() != 0x02)
        throw new InvalidOperationException("无效的DER签名格式");

    int rLength = reader.ReadByte();
    byte[] r = reader.ReadBytes(rLength);
    if (r.Length < 32)
        r = new byte[32 - r.Length].Concat(r).ToArray();

    if (reader.ReadByte() != 0x02)
        throw new InvalidOperationException("无效的DER签名格式");

    int sLength = reader.ReadByte();
    byte[] s = reader.ReadBytes(sLength);
    if (s.Length < 32)
        s = new byte[32 - s.Length].Concat(s).ToArray();

    byte[] compactSignature = new byte[64];
    Buffer.BlockCopy(r, 0, compactSignature, 0, 32);
    Buffer.BlockCopy(s, 0, compactSignature, 32, 32);
    return compactSignature;
}

验证步骤

  1. 运行代码生成JWT,复制到jwt.io验证,此时应能正常通过签名校验。
  2. 发送推送请求时,严格按照要求设置Authorization和Crypto-Key头,避免"Unknown auth scheme"错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 13:48:09