Apparently (!) inconsistent signing between .NET and Mono; Mono signing is not idempotent

Google Cloud Storage provides Java, C# code samples for generating signed URLs:

https://cloud.google.com/storage/docs/access-control?hl=en#signing-code-csharp

I'm using the code sample. With the same service account|key, bucket and object, the Java code and the C# code (on Windows) work. When I try the C# code on Mono/Linux, it does not work. The error is:

Code: SignatureDoesNotMatch
Message: The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.

Injecting some debugging code corroborates this error.

Here's the augmented method that does the signing:

private String signString(String stringToSign)  {
    if (key == null) throw new Exception("Certificate not initialized");
    CspParameters cp = new CspParameters(
        24,
        "Microsoft Enhanced RSA and AES Cryptographic Provider",
        ((RSACryptoServiceProvider)key.PrivateKey).CspKeyContainerInfo.KeyContainerName
        );
    RSACryptoServiceProvider provider = new RSACryptoServiceProvider(cp);
    byte[] buffer = Encoding.UTF8.GetBytes(stringToSign);
    byte[] rawSignature = provider.SignData(buffer, CryptoConfig.MapNameToOID("SHA256"));
    Console.WriteLine ("signature == ");
    Console.WriteLine (BitConverter.ToString(rawSignature).Replace("-", string.Empty));
    return Convert.ToBase64String(rawSignature);
}

I expect (perhaps incorrectly that) repeated calls to signString with the same string value, would return the same rawSignature and result. On Java and Windows, this is true. On Mono, the value changes

String testString="helloworld";
Console.WriteLine("Signing '" + testString + "' == " + this.signString(testString));
Console.WriteLine("Signing '" + testString + "' == " + this.signString(testString));
Console.WriteLine("Signing '" + testString + "' == " + this.signString(testString));

returns, abbreviated results:

signature == 
4415768E8E2FB862...
Signing 'helloworld' == RBV2jo4v...
signature == 
95E589C2F8DAD7ED...
Signing 'helloworld' == leWJwvja...
signature == 
0589E4454FE4FB3A...
Signing 'helloworld' == BYnkRU/k...

With Java, a similar test using the Google sample returns:

rawSignature == 
3E56F09EE9CF7D98...
Signing 'helloworld' == PlbwnunP...
rawSignature == 
3E56F09EE9CF7D98...
Signing 'helloworld' == PlbwnunP...
rawSignature == 
3E56F09EE9CF7D98...
Signing 'helloworld' == PlbwnunP...

What am I doing wrong?

Jon Skeet
people
quotationmark

I've managed to get consistent results by not using CspParameters at all, but using the PrivateKey property of X509Certificate2. My current "portable" implementation unfortunately requires a cast which makes me nervous, but it does appear to give the same results on Windows and Linux (under Mono 4.0.2), and those are the same results as the original sample code. Here's a short test app which works under Mono:

using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

public class Test
{
    static void Main()
    {
        var key = new X509Certificate2("key.p12", "notasecret");
        byte[] buffer = Encoding.UTF8.GetBytes("test string");
        // This is the slightly dodgy bit...
        var rsa = (RSACryptoServiceProvider) key.PrivateKey;
        byte[] signature = rsa.SignData(buffer, "SHA256");
        Console.WriteLine(Convert.ToBase64String(signature));
    }
}

As noted in comments, this doesn't work under .NET, for unknown reasons :(

Now I haven't tried using this to make any Cloud Storage requests yet, but I expect it would work.

On more modern platforms, you can use the fact that RSA has a SignData method, and use the GetPrivateRSAKey() extension method to get an RSA instance:

var rsa = key.GetRSAPrivateKey();
var signature = rsa.SignData(
   buffer, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

You may be able to use this on Mono too depending on which version you target.

people

See more on this question at Stackoverflow