(I've already been thru a lot of Stackoverflow/google results trying to find a fix for this.)
I am validating JWTs signed with RS256 using the default C# JwtSecurityTokenHandler. In some cases, the validation fails when it shouldn't. Concretely, tokens from a given Authorization Server validate properly while tokens form another Authorization Server won't.
BUT... Using the same JWTs and RSA Certificates on JWT.IO validates ALL the tokens succesfully. This is the part that makes me believe that there's something wrong/unusual in the C# implementation. I am also able to validate the same JWTs using the same Certificates using the oidc-client JavaScript library. The one place where the validation sometimes fails is in C#.
I traced the error down to JwtSecurityTokenHandler's ValidateSignature method. Searching the original github code and googling about RSA, I came down with this bare-bone method which allows me to reproduce the problem in a plain console app:
static void ValidateJWT(string token, string modulus, string exponent)
{
string tokenStr = token;
JwtSecurityToken st = new JwtSecurityToken(tokenStr);
string[] tokenParts = tokenStr.Split('.');
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters()
{
Modulus = FromBase64Url(modulus),
Exponent = FromBase64Url(exponent)
});
SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
var valid = rsaDeformatter.VerifySignature(hash, FromBase64Url(tokenParts[2]));
Console.WriteLine(valid); // sometimes false when it should be true
}
private static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
It is from that RSACryptoServiceProvider and using RSAKeys from here (https://gist.github.com/therightstuff/aa65356e95f8d0aae888e9f61aa29414) that I was able to Export the Public Key that allows me to validate JWTs successfully on JWT.IO.
string publicKey = RSAKeys.ExportPublicKey(rsa);
I can't provide actual JWTs to this post (they expire anyways), but does anyone knows of a crypto behavior specific to C# that could explain these validation errors, which don't happen in JavaScript nor on JWT.IO ?
And if so, any solution for this?
Thanks,
Martin
https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.1
Note that implementers have found that some cryptographic libraries
prefix an extra zero-valued octet to the modulus representations they
return, for instance, returning 257 octets for a 2048-bit key, rather
than 256. Implementations using such libraries will need to take
care to omit the extra octet from the base64url-encoded
representation.
In the case of one of the tokens you provided on a copy of this issue elsewhere, the decode of the modulus includes a prefixed 0x00 byte. This causes downstream problems. But you can fix their non-conformance.
byte[] modulusBytes = FromBase64Url(modulus);
if (modulusBytes[0] == 0)
{
byte[] tmp = new byte[modulusBytes.Length - 1];
Buffer.BlockCopy(modulusBytes, 1, tmp, 0, tmp.Length);
modulusBytes = tmp;
}
It looks like RS256 treats the signature as opaque bytes, so it will encode it as-is. So you probably don't need this correction (though it's where my investigation started):
byte[] sig = FromBase64Url(tokenParts[2]);
if (sig.Length < modulusBytes.Length)
{
byte[] tmp = new byte[modulusBytes.Length];
Buffer.BlockCopy(sig, 0, tmp, tmp.Length - sig.Length, sig.Length);
sig = tmp;
}
Related
I have a node server and and passing up usernames and passwords from unity. here is what I have so far. I am still trying to learn and understand encryption and there are so many types and Im just confused. The code below will successfully encrypt and decrypt the string. Is this code a good code to use for something like this or is there a better alternative? What type of encryption is this actually using? How would I decrypt this on node js? Any additional example, links, or comments would be much appreciated. Thanks!
public string encrypt(string toEncrypt) {
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = "ThisIsAKey"; // This is the key used to encrypt and decrypt can be anything.
var provider = new RSACryptoServiceProvider(cspParams);
byte[] tempencryptedBytes = provider.Encrypt(System.Text.Encoding.UTF8.GetBytes(toEncrypt), true);
string encrypted = Convert.ToBase64String(tempencryptedBytes); // convert to base64string for storage
Debug.Log("encrypted: " + encrypted);
// Get the value stored in RegString and decrypt it using the key.
return encrypted;
}
public string decrypt(string toDecrypt) {
CspParameters cspParams = new CspParameters();
cspParams.KeyContainerName = "ThisIsAKey"; // This is the key used to encrypt and decrypt can be anything.
var provider = new RSACryptoServiceProvider(cspParams);
string decrypted = System.Text.Encoding.UTF7.GetString(provider.Decrypt(Convert.FromBase64String(toDecrypt), true));
Debug.Log("decrypted: " + decrypted);
return decrypted;
}
EDIT: SHA256 code that i used added here. It doesnt output the correct string value.
SHA256 sha256 = SHA256Managed.Create();
byte[] bytes = System.Text.Encoding.UTF8.GetBytes("randy");
byte[] hash = sha256.ComputeHash(bytes);
string result = "";
for (int i = 0; i < hash.Length; i++) {
result += String.Format("{0:x2}", i);
}
Debug.Log("hash: " + result);
string result2 = Convert.ToBase64String(hash);
Debug.Log("hash: " + result2);
If something is good to be used depends on the context.
If you need to pass a username / password combination then RSA encryption may indeed be used, preferably in addition to TLS transport security for the connection. If you just need to verify a username or password then you may need a password hash (or PBKDF) instead. The .NET version of PBKDF2 can be found in this badly named class.
Even if all the cryptographic algorithms are secure then your system may still not be secure. For instance, if you cannot trust the public key that you are encrypting with then you may be encrypting with a public key of an attacker.
So your code is using this specific encrypt call using a boolean to select the encryption algorithm. As the boolean is set to true that means that RSA with OAEP is being used, using the default SHA-1 hash function internally (which is secure for OAEP encryption, even if SHA-1 isn't). It's better to use the newer call where you can specify the padding without the boolean anti-pattern. In that case you can also specify the internal hash function to be used.
RSA with OAEP is specified in PKCS#1 v2.2, which is specified in turn in RFC 8017. This will even specify the byte order to be used (RSA operates on numbers in the end, which can be encoded to bytes in different ways). As long as you use a compliant library in any runtime and know how to encode / decode the plaintext and ciphertext (when using text) then you should be able to decrypt using any runtime that implements RSA with OAEP, if you have the matching private key of course.
As a general rule, passwords shouldn't be decryptable. You should hash the password (using something like SHA256), then compare that to a stored hash in your Node.js code. Never store or transfer passwords plaintext or in a method that can be converted back to the original password.
In C#, hashing will look something like:
string toHash = "PasswordToBehashed";
SHA256 sha = new SHA256();
byte[] tempencryptedBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(toHash));
For reference, see the SHA256 class and an example using MD5 instead of SHA256.
I have a PEM file which includes my DSA private Key and i need to sign a string with this private key to send it to my partner API. (code attached)
For some reason i'm always getting Signature Invalid form my partner API. which means the sign is not in a good format.
My partner offers me to use bouncycastle for c# but i couldn't find any examples of how to sign the string with DSA from external PEM file.
Can i get any examples here?
Thanks.
I've tried to sign the string with a method i wrote:
public string ComputeSignature(string method, string path,string client_id, string timestamp, string username)
{
var data = String.Concat(method, path, client_id, timestamp);
if (!string.IsNullOrEmpty(username))
{
data = data + username;
}
string sig;
var encoding = new ASCIIEncoding();
byte[] dataInBytes = encoding.GetBytes(data);
using (System.Security.Cryptography.HMAC Hmac = new HMACSHA1())
{
Hmac.Key = encoding.GetBytes(privateKeyClean);
byte[] hash = Hmac.ComputeHash(dataInBytes, 0, dataInBytes.Length);
sig = Convert.ToBase64String(hash, 0, hash.Length);
}
return sig;
}
I solved this issue using bouncycastle:
private static string ComputeSignature()
{
AsymmetricCipherKeyPair asymmetricCipherKeyPair;
using (TextReader textReader = new StreamReader("myprivatekey.pem"))
{
asymmetricCipherKeyPair = (AsymmetricCipherKeyPair)new PemReader(textReader).ReadObject();
}
DsaPrivateKeyParameters parameters = (DsaPrivateKeyParameters)asymmetricCipherKeyPair.Private;
string text = TEXTTOENC;
if (!string.IsNullOrEmpty(userName))
{
text += userName;
}
Console.Out.WriteLine("Data: {0}", text);
byte[] bytes = Encoding.ASCII.GetBytes(text);
DsaDigestSigner dsaDigestSigner = new DsaDigestSigner(new DsaSigner(), new Sha1Digest());
dsaDigestSigner.Init(true, parameters);
dsaDigestSigner.BlockUpdate(bytes, 0, bytes.Length);
byte[] inArray = dsaDigestSigner.GenerateSignature();
string text2 = Convert.ToBase64String(inArray);
Console.WriteLine("Signature: {0}", text2);
return text2;
}
The reason that the DSA signature fails here is you aren't doing DSA at all. You're doing HMAC-SHA-1 using the contents of a key file as the HMAC key.
Read the DSA key parameters from the file.
.NET has no intrinsic support for reading PEM key files. But your title mentions BouncyCastle, so you can probably adapt the accepted answer to How to read a PEM RSA private key from .NET to DSA (using the PemReader).
Alternatively, you could use OpenSSL to make a self-signed certificate based off of this key, and put the cert+key into a PFX. .NET can load PFXes just fine.
Populate a DSA object.
If you go the PFX route, cert.GetDSAPrivateKey() will do the right thing (.NET 4.6.2). On versions older than 4.6.2 you can use cert.PrivateKey as DSA, which will only work for FIPS 186-2-compatible DSA (DSA was upgraded in FIPS 186-3).
If you're using BouncyCastle to read the PEM key parameters you can then stick with BouncyCastle or do something like using (DSA dsa = DSA.Create()) { dsa.ImportParameters(parametersFromTheFile); /* other stuff here */ }. (DSA.Create() will give an object limited to FIPS-186-2 on .NET Framework, but it can do FIPS-186-3 on .NET Core).
Sign the data
FIPS-186-2 only allows SHA-1 as the hash algorithm, FIPS-186-3 allows the SHA-2 family (SHA256, SHA384, SHA512). We'll assume that you're using FIPS-186-2/SHA-1 (if not, the substitutions required are hopefully obvious).
BouncyCastle: However they compute signatures. (Sorry, I'm not familiar with their APIs)
.NET 4.6.1 or older:
using (SHA1 hash = SHA1.Create())
{
signature = dsa.CreateSignature(hash.ComputeHash(dataInBytes));
}
.NET 4.6.2 or newer:
signature = dsa.SignData(dataInBytes, HashAlgorithmName.SHA1);
Soapbox
Then, once that's all said and done (or, perhaps, before): Ask yourself "why am I using DSA?". In FIPS 186-2 (and prior) DSA is limited to a 1024-bit key size and the SHA-1 hash. NIST SP800-57 classifies SHA-1 and DSA-1024 both as having 80 bits of security (Tables 2 and 3). NIST classified 80 bits of security as "Deprecated" for 2011-2013 and "Disallowed" for 2014 and beyond (Table 4). Modern usage of DSA (for entities subject to NIST recommendations) requires FIPS-186-3 support.
ECDSA gets ~keysize/2 bits of security; so the lowest (commonly supported) ECDSA keysize (keys based on NIST P-256/secp256r1) gets 128 bits of security, which NIST rates as good for 2031+.
RSA is also a better choice than DSA, because it has much better breadth of support for signatures still considered secure by NIST.
Though, if you're conforming to a protocol I guess there aren't a lot of options.
I need help in retrieving AES128-EBC encrypted string under Universal Windows Application.
I have a password in string that is used as a key. With it's 32 bits length MD5 hash value I would like to encrypt text with AES128-EBC.
Now I am using this for creating MD5Hash:
public string GetMD5Hash(String strMsg)
{
string strAlgName = HashAlgorithmNames.Md5;
IBuffer buffUtf8Msg = CryptographicBuffer.ConvertStringToBinary(strMsg, BinaryStringEncoding.Utf8);
HashAlgorithmProvider objAlgProv = HashAlgorithmProvider.OpenAlgorithm
string strAlgNameUsed = objAlgProv.AlgorithmName;
IBuffer buffHash = objAlgProv.HashData(buffUtf8Msg);
if (buffHash.Length != objAlgProv.HashLength)
{
throw new Exception("There was an error creating the hash");
}
string hex = CryptographicBuffer.EncodeToHexString(buffHash);
return hex;
}
And this code for encryption:
public string Encrypt(string input, string pass)
{
SymmetricKeyAlgorithmProvider provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesEcbPkcs7);
CryptographicKey key;
string encrypted = "";
byte[] keyhash = Encoding.ASCII.GetBytes(GetMD5Hash(pass));
key = provider.CreateSymmetricKey(CryptographicBuffer.CreateFromByteArray(keyhash));
IBuffer data = CryptographicBuffer.CreateFromByteArray(Encoding.Unicode.GetBytes(input));
encrypted = CryptographicBuffer.EncodeToBase64String(CryptographicEngine.Encrypt(key, data, null));
return encrypted;
}
The cause why I am using SymmetricAlgorithmNames.AesEcbPkcs7 is when I am using SymmetricAlgorithmNames.AesEcb the output string is empty. I don't understand why.
My question is: Does my code create an AES128-ECB encryption? Because I not really sure it does. Because the software that is waiting for that encrypted data not recognizes it, so it cannot decrypt it.
My question is: Does my code create an AES128-ECB encryption? Because I not really sure it does. Because the software that is waiting for that encrypted data not recognizes it, so it cannot decrypt it.
Yes, your code create an AES encryption with ECB cipher mode and PKCS7 padding. If I correctly understand your problem, you said this works with AesEcbPkcs7, but failed using AesEcb, your software for decryption doesn't work for this.
The difference between AesEcbPkcs7 and AesEcb is, AesEcbPkcs7 use PKCS#7 block padding modes, and PKCS #7 algorithms automatically pads the message to an appropriate length, so you don’t need to pad the cipher to a multiple of the block-size of the algorithm you are using. So if you insist to use AesEcb to encrypt, I recommend to use `AesEcbPkcs7, otherwise an exception: The supplied user buffer is not valid for the requested operation.
So I guess, one possibility here in your decryption software, it may have the ability to use AesEcbPkcs7, but it doesn't implement the decrytion of AesEcb. Here I tested decryption based on your code, this code can decrypt AesEcb correctly:
public string Decrypt(string input, string pass)
{
var keyHash = Encoding.ASCII.GetBytes(GetMD5Hash(pass));
// Create a buffer that contains the encoded message to be decrypted.
IBuffer toDecryptBuffer = CryptographicBuffer.DecodeFromBase64String(input);
// Open a symmetric algorithm provider for the specified algorithm.
SymmetricKeyAlgorithmProvider aes = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesEcb);
// Create a symmetric key.
var symetricKey = aes.CreateSymmetricKey(keyHash.AsBuffer());
var buffDecrypted = CryptographicEngine.Decrypt(symetricKey, toDecryptBuffer, null);
string strDecrypted = CryptographicBuffer.ConvertBinaryToString(BinaryStringEncoding.Utf8, buffDecrypted);
return strDecrypted;
}
Another possibility I think you catch the exception when using AesEcb and the user buffer is not valid for the requested operation and handled it when you call your Encrypt(string input, string pass) method, the encryption failed actually.
I'm trying to decrypt a message as part of a key exchange. I've got a 2048 bit RSA private key which I used to generate a certificate. I receive a message as part of a HTTP request which I need to decrypt with my private key. However, I receive the following error message when executing the last line:
"The data to be decrypted exceeds the maximum for this modulus of 256 bytes."
I've tried reducing the byte array of the data to decrypt as well as reversing it. If I do any of those two, I receive a "Bad Data" error.
Any help would be greatly appreciated.
Example of message to decode:
ajJDR09EQkUzT0prRHJlM2I1bzZGYjlaUWFpQTB6U2pQb0JGeDBvQ0tseEpYMGhmUkdSU0VJRnFnOEdQTDV5SlRJZmxoQUYzeFAxS3NGM1hFSnBobGl3Z3Y2UStydkY3ZkgvVmRLSit6bE5MZ3RTN0twUWZUaUZqMjlkLzBGVWVhL25qdnFXYTVrdlBrYUN2T2grZ1Rnc3FEd3U4ZVZiOUxhWVUzQWpRODk3MFY4VjM5c1VWYXRLcXdZbitQQkV4cFFSYXRJUlcyS2taSXpuRGZTVCt3dGZRcHMwU1lra3ZENSt6VHZnSGFRSmZNQXMvUlRiSERPVTZrNWo5dVR3SXNTOCtlalBWYjdMc1phOXU1c1plVTZpTlhvOUp1emxDalZpaVk3YnY0SkJCcHhqclRPaVA4NVhUYWg1TVhRYUZsMTZOVzE4dDMzYndnQmVkQmRwNEN3PT0=
C# code:
//http request containing the HMAC key which is encrypted against the public key
hmacKey = oCtx.RequestContext.RequestMessage.ToString();
hmacKey = hmacKey.Remove(0, 8);
hmacKey = hmacKey.Remove(hmacKey.Length - 9);
//decode into binary using Base64
byte[] data = Convert.FromBase64String(hmacKey);
string publicCert = "-----BEGIN CERTIFICATE-----......-----END CERTIFICATE-----";
string privateKey = "-----BEGIN RSA PRIVATE KEY-----......-----END RSA PRIVATE KEY-----";
byte[] certBuffer = Helpers.GetBytesFromPEM(publicCert, PemStringType.Certificate);
byte[] keyBuffer = Helpers.GetBytesFromPEM(privateKey, PemStringType.RsaPrivateKey);
X509Certificate2 x509cert = new X509Certificate2(certBuffer);
RSACryptoServiceProvider prov = Crypto.DecodeRsaPrivateKey(keyBuffer);
x509cert.PrivateKey = prov;
//tried to reduce the size of the data to decrypt as well as reversing it
//Array.Resize(ref data, 32);
//Array.Reverse(data);
byte[] result = prov.Decrypt(data, false);
More info on the GetBytesFromPEM method is available from this example:
http://www.codeproject.com/Articles/162194/Certificates-to-DB-and-Back
UPDATE:
Trying to decode twice, I get the following result:
code:
.....
byte[] data2 = Convert.FromBase64String(hmacKey);
string abc = Encoding.Default.GetString(data2);
byte[] data = Convert.FromBase64String(abc);
.....
byte[] result = prov.Decrypt(data, false);
string result2 = Encoding.Default.GetString(result);
result:
Óh#-šÚz;CÏ7
.«™"ã®ÿRè±àyéK.
The errors are basically due to encoding errors, both binary encoding (base 64) issues and character encoding issues (UTF-8/UTF-16).
Usually you would expect a binary HMAC to be encrypted. Instead the HMAC was hex encoded, which in turn was encoded using ASCII encoding (which is compatible with UTF-8). The .NET default is however UTF-16LE (what .NET incorrectly calls Unicode encoding).
The resulting ciphertext was base 64 encoded, which is what you would expect if the result needs to be transported in text. Instead double base 64 seemed to have been utilized. As the base 64 decoding resulted in another base 64 encoded string, the result was too large for the RSA decryption to handle.
I've got the following code sample in Java, and I need to re-enact it in C#:
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(pkcs8PrivateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privKey = keyFactory.generatePrivate(privKeySpec);
Signature sign = Signature.getInstance("MD5withRSA");
sign.initSign(privKey);
sign.update(data);
byte[] signature = sign.sign();
Is it possible with the standard .Net Crypto API, or should I use BouncyCastle?
Thanks,
b.
Another way is to use CNG (Cryptography Next Generation), along with the Security.Cryptography DLL from CodePlex
Then you can write:
byte[] dataToSign = Encoding.UTF8.GetBytes("Data to sign");
using (CngKey signingKey = CngKey.Import(pkcs8PrivateKey, CngKeyBlobFormat.Pkcs8PrivateBlob))
using (RSACng rsa = new RSACng(signingKey))
{
rsa.SignatureHashAlgorithm = CngAlgorithm.MD5;
return rsa.SignData(dataToSign);
}
Updated thanks to Simon Mourier: with .Net 4.6, you no longer need a separate library
I am running into a very similar problem trying to create a native C# tool for packing Chrome extensions (using SHA1, not MD5, but that's not a big difference). I believe I have tried literally every possible solution for .Net: System.Security.Cryptography, BouncyCastle, OpenSSL.Net and Chilkat RSA.
The best solution is probably Chilkat; their interface is the cleanest and most straightforward, it's well-supported and well-documented, and there are a million examples. For instance, here's some code using their library that does something very close to what you want: http://www.example-code.com/csharp/rsa_signPkcs8.asp. However, it's not free (though $150 is not unreasonable, seeing as I have burned 2 days trying to figure this out, and I make a bit more than $75 a day!).
As a free alternative, JavaScience offers up a number of crypto utilities in source form for multiple languages (including C#/.Net) at http://www.jensign.com/JavaScience/cryptoutils/index.html. The one that's most salient to what you are trying to do is opensslkey (http://www.jensign.com/opensslkey/index.html), which will let you generate a RSACryptoServiceProvider from a .pem file. You can then use that provider to sign your code:
string pemContents = new StreamReader("pkcs8privatekey.pem").ReadToEnd();
var der = opensslkey.DecodePkcs8PrivateKey(pemContents);
RSACryptoServiceProvider rsa = opensslkey.DecodePrivateKeyInfo(der);
signature = rsa.SignData(data, new MD5CryptoServiceProvider());
You can use this code . At the first you should download "BouncyCastle.Crypto.dll" from http://www.bouncycastle.org/csharp/ .
/// <summary>
/// MD5withRSA Signing
/// https://www.vrast.cn
/// keyle_xiao 2017.1.12
/// </summary>
public class MD5withRSASigning
{
public Encoding encoding = Encoding.UTF8;
public string SignerSymbol = "MD5withRSA";
public MD5withRSASigning() { }
public MD5withRSASigning(Encoding e, string s)
{
encoding = e;
SignerSymbol = s;
}
private AsymmetricKeyParameter CreateKEY(bool isPrivate, string key)
{
byte[] keyInfoByte = Convert.FromBase64String(key);
if (isPrivate)
return PrivateKeyFactory.CreateKey(keyInfoByte);
else
return PublicKeyFactory.CreateKey(keyInfoByte);
}
public string Sign(string content, string privatekey)
{
ISigner sig = SignerUtilities.GetSigner(SignerSymbol);
sig.Init(true, CreateKEY(true, privatekey));
var bytes = encoding.GetBytes(content);
sig.BlockUpdate(bytes, 0, bytes.Length);
byte[] signature = sig.GenerateSignature();
/* Base 64 encode the sig so its 8-bit clean */
var signedString = Convert.ToBase64String(signature);
return signedString;
}
public bool Verify(string content, string signData, string publickey)
{
ISigner signer = SignerUtilities.GetSigner(SignerSymbol);
signer.Init(false, CreateKEY(false, publickey));
var expectedSig = Convert.FromBase64String(signData);
/* Get the bytes to be signed from the string */
var msgBytes = encoding.GetBytes(content);
/* Calculate the signature and see if it matches */
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
return signer.VerifySignature(expectedSig);
}
}
This SO question answers the PKCS#8 part of your code. The rest of the .NET RSA classes are a bizarre jumble of partially overlapping classes that are very difficult to fathom. It certainly appears that signature support is in either of the RSACryptoServiceProvider and/or RSAPKCS1SignatureFormatter classes.
Disclaimer: I know Java and cryptography, but my knowledge of C# and .NET is very limited. I am writing here only under the influence of my Google-fu skills.
Assuming that you could decode a PKCS#8-encoded RSA private key, then, from what I read on MSDN, the rest of the code should look like this:
byte[] hv = MD5.Create().ComputeHash(data);
RSACryptoServiceProvider rsp = new RSACryptoServiceProvider();
RSAParameters rsp = new RSAParameters();
// here fill rsp fields by decoding pkcs8PrivateKey
rsp.ImportParameters(key);
RSAPKCS1SignatureFormatter rf = new RSAPKCS1SignatureFormatter(rsp);
rf.SetHashAlgorithm("MD5");
byte[] signature = rf.CreateSignature(hv);
The relevant classes are in the System.Security.Cryptography namespace.
As for the PKCS#8 key blob decoding (i.e. filling in the rsp fields), I found this page which describes a command-line utility in C# which can perform that job. The source code is provided and is a single C# file. From what I read in it, that code decodes the PKCS#8 file "manually"; indirectly, this should mean that raw .NET (2.0) does not have facilities for PKCS#8 key file decoding (otherwise the author of that tool would not have went to the trouble of implementing such decoding). For your task at hand, you could scavenge from that source file the parts that you need, skipping anything about PEM and symmetric encryption; your entry point would be the DecodePrivateKeyInfo() function, which apparently expects a DER-encoded unencrypted PKCS#8 file, just like Java's PKCS8EncodedKeySpec.