Search code examples
encryptiongraphsubscription

Graph event subscription - can't decrypt the webhook payload


I'm creating a Graph subscription on Event resource, specifying IncludeResourceData=true. This of course requires me to supply a (base 64 encoded) certificate public key that they will use on their end to encrypt the resource data in the webhook payload. The subscription is created with no error, and I receive the webhook calls as expected, when I create or update an event. I'm basically using the sample payload decryption code copied from Microsoft's article; however I'm having no luck decrypting the encrypted part of the webhook payload. It never even gets to the point of trying to decrypt the payload, as the "actualSignature" and "expectedSignature" don't match. Specifics:

I have the code that creates the subscription and the code that listens for the webhook call running on the same PC, and they're loading the same certificate (pfx file) from disk at run time. This is how I'm getting the public key to use for the subscription creation:

X509Certificate2 certif = new X509Certificate2(@"C:\test\keys\GraphEncryption-20230221.pfx", "", X509KeyStorageFlags.PersistKeySet);
byte[] exp = certif.Export(X509ContentType.Cert);
string expString = Convert.ToBase64String(exp);

So expString is what I supply for the subscription EncryptionCertificate property.

On the webhook receiver side, I use this:

X509Certificate2 certif = new X509Certificate2(@"C:\test\keys\GraphEncryption-20230221.pfx", "", X509KeyStorageFlags.PersistKeySet);
RSACryptoServiceProvider rsaProvider = (RSACryptoServiceProvider)certif.PrivateKey;

In both cases, the pfx file loads with no error. But upon receiving a webhook and trying to decrypt, I end up with these expected vs. actual (converted to hex strings for readability):

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

So not only do they not match, they're not even the same length (the expected signature is 44 bytes long and the actual is only 32 bytes long).

I've tried this with different certificates created different ways (via openssl and Azure keyvault). Here's my full code on the webhook receiver side.

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);  //(< value from dataKey property>);

                // 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); // < the value from the data property, still encrypted>;
                byte[] expectedSignature = Encoding.ASCII.GetBytes(val.encryptedContent.dataSignature); //< the value from the dataSignature property >;
                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");

And these are some classes I created for the purpose of deserializing the webhook payload.

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

Solution

  • I figured out my problem. I was not properly converting the base64 encoded values from the payload to byte arrays. For some reason I was using Encoding.ASCII.GetBytes when I should have been using Convert.FromBase64String.

    For these lines in Microsoft's sample decryption code:

    byte[] encryptedPayload = <the value from the data property, still encrypted>;
    byte[] expectedSignature = <the value from the dataSignature property>;
    

    I was using trying to do it like this:

    byte[] encryptedPayload = Encoding.ASCII.GetBytes(val.encryptedContent.data);
    byte[] expectedSignature = Encoding.ASCII.GetBytes(val.encryptedContent.dataSignature);
    

    This was not correct. This is what I finally used, that worked:

    byte[] encryptedPayload = Convert.FromBase64String(val.encryptedContent.data);
    byte[] expectedSignature = Convert.FromBase64String(val.encryptedContent.dataSignature);