Search code examples
c#.netmicrosoft-graph-apix509certificatex509

Cannot create Microsoft Graph API subscription using self signed C# X509Certificate2


I am trying to create subscriptions in the Graph API, and I'm looking at trying to include the resource data. I've been reading through this Microsoft documentation to try and create the keys in code, and have come up with the following code, which was created with the help from this stack overflow answer:

using var rsa = RSA.Create(4096);

var certificateRequest = new CertificateRequest("CN=testCertificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

// Make sure the key can only be used for encryption purposes
certificateRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.EncipherOnly, true));

var selfSignedCertificate = certificateRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));

using var exportedCertificate = new X509Certificate2(selfSignedCertificate.Export(X509ContentType.Pfx));

var x509PublicKey = Convert.ToBase64String(exportedCertificate.GetPublicKey());
var rsaPrivateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());

I have tried:

  • Using different hash algorithms
  • Not specifying the X509KeyUsageExtension
  • Exporting the certificate as X509ContentType.Cert and using that as the public key

Once I have created this certificate, I am then using the following code to try to create a subscription:

var requestBody = new Subscription
{
    ChangeType = "created,updated",
    NotificationUrl = _notificationUrl,
    LifecycleNotificationUrl = _lifecycleUrl,
    Resource = resource,
    ExpirationDateTime = DateTime.UtcNow.AddDays(3),
    ClientState = _clientState,
    IncludeResourceData = true,
    EncryptionCertificate = x509PublicKey,
    EncryptionCertificateId = certificateId
};

graphClient.Subscriptions.PostAsync(requestBody, cancellationToken: cancellationToken);

This works if I remove the includeResourceData,EncryptionCertificate and EncryptionCertificateId, but if not I am getting a HTTP 400 response that says:

Certificate validation error: Cannot find the requested object.

If anyone has any ideas or examples of how to get around this error, it would be greatly appreciated.


Solution

  • I have found the key to my issue and why this wasn't working. After doing some more research into how to do this, I found a README file in some example code, which has a bit more detail about what Microsoft are expecting. The key part to this README states at the end:

    Copy the content between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----. This will be the value of Base64EncodedCertificate in appsettings.json file.

    After doing some digging around in the code and what was being returned, I noticed that the public key in the certificate was a different base64 string to the certificate itself, which contains the public key within it. After looking back at the Microsoft documentation, it states to:

    Export the certificate in Base64-encoded X.509 format, and include only the public key

    I had misunderstood what they were asking for, and assumed they only wanted the public key from what the certificate was made from.

    The only issue after this was working out how to extract the base64 contents of the certificate, and I've not found any helpful methods within the X509Certificate2 class to do this, so I wrote a little helper method to sort this:

    private const string _beginCertificate = "-----BEGIN CERTIFICATE-----\n";
    private const string _endCertificate = "\n-----END CERTIFICATE-----";
    
    /// <summary>
    ///     Extract the base64 certificate without the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- strings.
    /// </summary>
    /// <returns>
    ///     The base64 contents of the X509 certificate provided
    /// </returns>
    private static string ExtractBase64Certificate(string x509CertificatePemString)
    {
        var indexFrom = x509CertificatePemString.IndexOf(_beginCertificate, StringComparison.OrdinalIgnoreCase) + _beginCertificate.Length;
        var indexTo = x509CertificatePemString.LastIndexOf(_endCertificate, StringComparison.OrdinalIgnoreCase);
    
        return x509CertificatePemString.Substring(indexFrom, indexTo - indexFrom);
    }
    

    Using this to extract the certificate, I have since been able to get passed the "Certificate validation error" and create the subscription. I have tidied up the code from the original question, to ensure the certificate can only do what is required, so the code now looks like this:

    using var rsa = RSA.Create(4096);
    
    var certificateRequest = new CertificateRequest("CN=testCertificate", rsa, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
    
    // Make sure the key can only be used for encryption purposes
    certificateRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.EncipherOnly, true));
    
    var selfSignedCertificate = certificateRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));
    
    using var exportedCertificate = new X509Certificate2(selfSignedCertificate.Export(X509ContentType.Cert));
    
    var base64PublicKeyCertificate = ExtractBase64Certificate(exportedCertificate.ExportCertificatePem());
    var rsaPrivateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
    

    There are two key things to note with this updated code:

    • Microsoft accepts using SHA512 as the hashing algorithm, so it's better to use this. I'm not sure if they would use SHA3_512 but my machine can't use this hashing algorithm so I have stuck to SHA_512.
    • When exporting the certificate, making sure to use X509ContentType.Cert, as this only exports the public key, to ensure you are not also sending the private key in the request.