Graph Event订阅Webhook负载解密失败:签名不匹配求助
问题描述
创建Graph Event订阅时设置IncludeResourceData=true,提供Base64编码的证书公钥后,订阅创建成功且能正常接收Webhook回调,但使用微软官方示例解密代码时,actualSignature与expectedSignature始终不匹配,二者长度分别为32字节和44字节。
环境与代码信息
- 订阅创建与Webhook接收使用同一份PFX证书,运行在同一设备:
- 订阅创建时获取公钥代码:
X509Certificate2 certif = new X509Certificate2(@"C:\test\keys\GraphEncryption-20230221.pfx", "", X509KeyStorageFlags.PersistKeySet); byte[] exp = certif.Export(X509ContentType.Cert); string expString = Convert.ToBase64String(exp);expString作为订阅的EncryptionCertificate属性值。 - Webhook接收端加载证书代码:
X509Certificate2 certif = new X509Certificate2(@"C:\test\keys\GraphEncryption-20230221.pfx", "", X509KeyStorageFlags.PersistKeySet); RSACryptoServiceProvider rsaProvider = (RSACryptoServiceProvider)certif.PrivateKey;
- 订阅创建时获取公钥代码:
签名差异
接收Webhook后得到的十六进制签名:
expectedSignature 53-55-52-79-62-50-59-51-4A-4A-39-62-57-34-69-69-66-34-31-30-30-62-47-4D-4B-4A-4F-73-52-47-33-69-48-6E-46-4C-33-7A-4F-4D-63-64-4D-3D actualSignature 8A-EE-D9-FE-47-C9-F8-83-2E-27-3C-43-6E-F9-95-E7-92-9C-85-ED-E0-70-17-39-64-54-8B-65-B8-A9-EB-E4
已尝试通过OpenSSL和Azure Key Vault生成不同证书,问题依旧。
Webhook接收端完整代码
X509Certificate2 certif = new X509Certificate2(@"C:\test\keys\GraphEncryption-20230221.pfx", "", X509KeyStorageFlags.PersistKeySet); RSACryptoServiceProvider rsaProvider = (RSACryptoServiceProvider)certif.PrivateKey; if (Request.RequestType != "POST") return; string postdata; using (StreamReader stream = new StreamReader(Request.InputStream)) postdata = stream.ReadToEnd(); if (string.IsNullOrEmpty(postdata)) return; System.Diagnostics.Debug.WriteLine(postdata); GraphEvent ev = JsonConvert.DeserializeObject<GraphEvent>(postdata); foreach (GraphSubs val in ev.value) { byte[] encryptedSymmetricKey = Convert.FromBase64String(val.encryptedContent.dataKey); // Decrypt using OAEP padding. byte[] decryptedSymmetricKey = rsaProvider.Decrypt(encryptedSymmetricKey, fOAEP: true); // Can now use decryptedSymmetricKey with the AES algorithm. byte[] encryptedPayload = Encoding.ASCII.GetBytes(val.encryptedContent.data); byte[] expectedSignature = Encoding.ASCII.GetBytes(val.encryptedContent.dataSignature); byte[] actualSignature; using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey)) { actualSignature = hmac.ComputeHash(encryptedPayload); } Debug.WriteLine("expectedSignature " + BitConverter.ToString(expectedSignature)); Debug.WriteLine("actualSignature " + BitConverter.ToString(actualSignature)); if (actualSignature.SequenceEqual(expectedSignature)) { AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider(); aesProvider.Key = decryptedSymmetricKey; aesProvider.Padding = PaddingMode.PKCS7; aesProvider.Mode = CipherMode.CBC; // Obtain the intialization vector from the symmetric key itself. int vectorSize = 16; byte[] iv = new byte[vectorSize]; Array.Copy(decryptedSymmetricKey, iv, vectorSize); aesProvider.IV = iv; string decryptedResourceData; // Decrypt the resource data content. using (var decryptor = aesProvider.CreateDecryptor()) { using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload)) { using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt = new StreamReader(csDecrypt)) { decryptedResourceData = srDecrypt.ReadToEnd(); } } } } // decryptedResourceData now contains a JSON string that represents the resource. System.Diagnostics.Debug.Write("decrypted data: " + decryptedResourceData); } else Debug.WriteLine("! no decrypt performed: actualSignature not equal expectedSignature"); }
反序列化类定义
class GraphSubs { public GraphSubs() { } public string subscriptionId; public DateTimeOffset subscriptionExpirationDateTime; public string changeType; public string resource; public EncryptedContent encryptedContent; public ResourceData resourceData; public string clientState; public string tenantId; public string lifecycleEvent; } class ResourceData { [JsonProperty("@odata.type")] public string dataType; [JsonProperty("@odata.id")] public string dataId; [JsonProperty("@odata.etag")] public string dataEtag; public string id; } class EncryptedContent { public string data; public string dataKey; public string dataSignature; public string encryptionCertificateId; public string encryptionCertificateThumbprint; } class GraphEvent { public GraphSubs[] value; }
问题排查与解决
核心原因
代码中对encryptedContent.data和encryptedContent.dataSignature的处理逻辑错误:
encryptedContent.data是Base64编码的加密负载,不能直接用Encoding.ASCII.GetBytes()转换,需先解码为原始字节数组。encryptedContent.dataSignature是Base64编码的HMAC签名,同样需先解码为字节数组,而非直接转换为ASCII字节。
这两个错误导致计算出的actualSignature与expectedSignature完全不匹配,且出现长度差异(44字节是Base64字符串的ASCII字节长度,32字节是HMACSHA256签名的正确长度)。
修复后的关键代码片段
修改接收端中处理encryptedPayload和expectedSignature的部分:
// 修正:将Base64编码的data解码为字节数组 byte[] encryptedPayload = Convert.FromBase64String(val.encryptedContent.data); // 修正:将Base64编码的dataSignature解码为字节数组 byte[] expectedSignature = Convert.FromBase64String(val.encryptedContent.dataSignature); byte[] actualSignature; using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey)) { actualSignature = hmac.ComputeHash(encryptedPayload); }
额外优化建议
- 避免使用已过时的
RSACryptoServiceProvider,改用RSA类:RSA rsa = certif.GetRSAPrivateKey(); byte[] decryptedSymmetricKey = rsa.Decrypt(encryptedSymmetricKey, RSAEncryptionPadding.OaepSHA1); AesCryptoServiceProvider已过时,推荐使用Aes类:using Aes aes = Aes.Create(); aes.Key = decryptedSymmetricKey; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; byte[] iv = new byte[16]; Array.Copy(decryptedSymmetricKey, iv, iv.Length); aes.IV = iv;
内容的提问来源于stack exchange,提问作者Teeces
相关产品推荐
相关产品推荐

