I have a problem verifying an RSA signature with C# by providing digest only. Here is my code in Java, which returns a "true" value, indicating that the signature is correct:
String certificateBase64 = "MIIC6jCCAdKgAwI...";
String signatureValueBase64 = "d0rLeNX+sQCZ3io0ji6j0P2M4bpGYxA+WKS6KkP4GYDxm4qrfhnvrKUOqvDmXNT3kNhv8I6Se6T4TFqJmYPTTQ8O914nDNmqASlknii6EA4ALExVhXfuZ7BeVNZy33AacBF8K/GIYH9DyvtWz9lVFaIX4BV9176CHo2FdSYXfNnrpJWQBIlpEhnsVHThCAabF/AOM/G2vgr5yO2Etbz3VCyZJesBOEmqRl6VlndFXZBgR527zknrH5fnACYrDacVXnFFY1OYGhaRI7tZrOvgkELnXV55aYhShWjAVcrGxaTC6vCHx3Ou4bsbjSCs4PXNMaaPcHUKH99xXODHFnMOjA==";
String hashBase64 = "MDEwDQYJYIZIAWUDBAIBBQAEIGlE+Lr0y76nMOdxNLu1k5g4l3feoWU4MYZm8yo0N5vE";
Base64.Decoder decoder = Base64.getDecoder();
CertificateFactory x509Factory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert;
try (ByteArrayInputStream bais = new ByteArrayInputStream(decoder.decode(certificateBase64))) {
x509Cert = (X509Certificate) x509Factory.generateCertificate(bais);
}
Signature signature = Signature.getInstance("NONEwithRSA");
signature.initVerify(x509Cert.getPublicKey());
signature.update(decoder.decode(hashBase64));
boolean valid = signature.verify(decoder.decode(signatureValueBase64));
System.out.println(valid);
However, when I try to validate signature with C# providing the same input parameters, I always receive "false". Here is my code:
string certificateBase64 = "MIIC6jCCAdKgAwI...";
string signatureValueBase64 = "d0rLeNX+sQCZ3io0ji6j0P2M4bpGYxA+WKS6KkP4GYDxm4qrfhnvrKUOqvDmXNT3kNhv8I6Se6T4TFqJmYPTTQ8O914nDNmqASlknii6EA4ALExVhXfuZ7BeVNZy33AacBF8K/GIYH9DyvtWz9lVFaIX4BV9176CHo2FdSYXfNnrpJWQBIlpEhnsVHThCAabF/AOM/G2vgr5yO2Etbz3VCyZJesBOEmqRl6VlndFXZBgR527zknrH5fnACYrDacVXnFFY1OYGhaRI7tZrOvgkELnXV55aYhShWjAVcrGxaTC6vCHx3Ou4bsbjSCs4PXNMaaPcHUKH99xXODHFnMOjA==";
string hashBase64 = "MDEwDQYJYIZIAWUDBAIBBQAEIGlE+Lr0y76nMOdxNLu1k5g4l3feoWU4MYZm8yo0N5vE";
byte[] certificateBytes = Convert.FromBase64String(certificateBase64);
byte[] signatureValueBytes = Convert.FromBase64String(signatureValueBase64);
byte[] hashBytes = Convert.FromBase64String(hashBase64);
X509Certificate2 x509Cert = new X509Certificate2(certificateBytes);
RSA publicKey = (RSA)x509Cert.GetRSAPublicKey();
bool isValid = publicKey.VerifyHash(hashBytes, signatureValueBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Console.WriteLine(isValid);
Do you have any ideas?
I tried to validate signature in C# using RSA, RSACryptoServiceProvider and RSAPKCS1Signatureformatter, but no luck with none of them. I'm not experienced C# programmer, so I do not know all the possibilities.
The Java code uses PKCS#1 v1.5 padding. To find a fix for the C# code, at least a rough understanding of PKCS#1 v1.5 padding is required (for an exact specification see RFC8017):
For PKCS#1 v1.5 padding, the message M is first hashed: H, then the digest ID is prepended:
T = ID || H (more precisely, T is the DER encoding of the DigestInfo).
When padding, T is prefixed with 0x00 || 0x01 || PS || 0x00, where PS consists of so many 0xFF values that the signature size is equal to the RSA key size (i.e. the modulus).
So the whole signature is: 0x00 || 0x01 || PS || 0x00 || T.
In the Java code, hashBase64
corresponds to the Base64 encoding of T, where the ID corresponds to that of SHA256. To see this, the easiest way is to Base64 decode hashBase64
and then hex encode the resulting data:
3031300d060960864801650304020105000420 6944f8baf4cbbea730e77134bbb59398389777dea16538318666f32a34379bc4
This value, i.e. T, becomes most understandable when viewed in an Asn.1 parser, e.g. https://lapo.it/asn1js/.
NONEwithRSA
defines that the data is directly interpreted as T (so neither the message is hashed nor an ID is prepended).
In this way, the signature 0x00 || 0x01 || PS || 0x00 || T is also generated, i.e. you get the same PKCS#1 v1.5 compliant signature as if you use the original data (from which the hash was generated) and the algorithm SHA256withRSA
.
Signing already hashed data is the actual purpose of NONEwithRSA
(namely when the signing authority is only allowed to know the hash, but not the actual data). Unfortunately, NONEwithRSA
is often misused, that is, for signing unhashed data.
In the C# code the same can be achieved with VerifyHash()
. However, not T must be passed, but only the hash. The digest ID is added implicitly by VerifyHash()
, which is the reason that the digest must be specified.
The bug in the current C# code is that T (i.e. hashBytes
) and not H is passed, so replace hashBytes
with hashBytes[^32..]
and verification is successful.
Sample code Java:
// Import public key
byte[] spkiDer = Base64.getDecoder().decode("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvgyRQ3SRGFFf/5pBql59itVdsb2aX9vmwNiN9isrnQotQw8zDgoHb50qNlorjU1K4QmRYtvKRu0RTqBbwk1UwSodcdLT3CdQ2zzD29GJdDg422qQHlJ9thIs6kAHhNqsRQsjzdyZA+VpkihncBTjGMjtETVdl/wAoIiiZHhTlwq6djTYMNc6Qo2oxpeXVNo1yBsoEh1atwNW682zhq8my3JY4vTV/KqAWlz1/k1QDaMkT+QJNohhjyEvU8p6xhNbj3LawBV3RikwNSAIs00osWXycSS8/y7pFZUxL0uJonfXIFFCx5rthdi7GxXBS8/D849oMSYdUvgmKZ2tyrcNyrtlDX13xhrH6l8l1U3FA5eAIc7i/Df4iSeLQsDb3yUv1PoX4G/3caPiWQ496dk5sYO5vs0IMhgBJ6xizVBHhjSxBDNtAJgAIq4UrgIY9SCu9AFi0hNbxpna1Y3+CBGH5jWylEkkQBDlW3D8rY6Jztqtzj1cbaRziqPcVbyi+NZDVgx0g8ER8qWi7g2oQvOb5m5mOVCPxQwjNFZqNNVOgHQ7OBzmJgZqzgvL0LPqbtpqN7wgWcb2DKcjEePwerpQiLEo3qIyAjqhltCCEXDJFhkSc/ChGL5M1pXUpsP/aQ0y3OZdXns+HegW7ifL6LVdlFCciuQO+xK3bhW3ZZIbYkcCAwEAAQ==");
KeyFactory kf = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(spkiDer);
RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509);
// Verify
byte[] hashBytes = Base64.getDecoder().decode("MDEwDQYJYIZIAWUDBAIBBQAEIGlE+Lr0y76nMOdxNLu1k5g4l3feoWU4MYZm8yo0N5vE");
byte[] signature = Base64.getDecoder().decode("N3Zc/d8UzODZvlndRhlc6b4uDX2Z6a5okA9al51W2B9hAKjxLygyoZmNkALn/BNyEAWhFQAiC6mFkF0/KmAVF0ci7ZVFlfLkiDSqo4QS67KPHUeDk15aARk/sB5B4yAyG9p4OvhX4gZ+IZNWFb7sqzJ/bdZ39bxxeLGKsChVlsaY4VvROWZRclI2Hox/80znMYfFBU6bQysp34F3+WQo8BE0gXnmwe99y9beyWK099mnjtDf79ZeQ0n34SyYAjf5Hh7xh13VaQ7pXaMkf1gKCXoClbLyKkn/HUgvBZliUhPGI1QTrLGuJSjvPHUA4RNu/jx7XNfp2Sav4yCikL1waOkl3notZBMZyH908wTRAvCvNO7Al453Af22otK5ybgU6HuVqmS6epQXXutUVpCWZKTYrcU+kmQmBqOjAaSRmbzoJw0h8uWJ9mpjdpxpr9U6Y5ri4A8DKt/TyFySA7tRNiuAh8iH737qmsDrpdvbKk661l9ftXWrDqiyVagqfe1/taNFrattQ9wFY4z2bdEHDKZvyLp15/hWZjeH2NvgC0mvuAq+KPgOlXqjgwfq7xS/8OUhYZhkRDvWKR2g3NidXG75RVUHggMIfu0blO/doE0u6cLqYaMprbw4fe+JtUKTkxyWa2Q9tbaPKMc6FepYOw1m2BKM0oDofrmZ5WDud5E=");
Signature verifier = Signature.getInstance("NONEwithRSA");
verifier.initVerify(pubKey);
verifier.update(hashBytes); // pass T = ID|H
boolean verified = verifier.verify(signature);
System.out.println(verified); // true
Sample code C#:
// Import public key
byte[] spkiDer = Convert.FromBase64String("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvgyRQ3SRGFFf/5pBql59itVdsb2aX9vmwNiN9isrnQotQw8zDgoHb50qNlorjU1K4QmRYtvKRu0RTqBbwk1UwSodcdLT3CdQ2zzD29GJdDg422qQHlJ9thIs6kAHhNqsRQsjzdyZA+VpkihncBTjGMjtETVdl/wAoIiiZHhTlwq6djTYMNc6Qo2oxpeXVNo1yBsoEh1atwNW682zhq8my3JY4vTV/KqAWlz1/k1QDaMkT+QJNohhjyEvU8p6xhNbj3LawBV3RikwNSAIs00osWXycSS8/y7pFZUxL0uJonfXIFFCx5rthdi7GxXBS8/D849oMSYdUvgmKZ2tyrcNyrtlDX13xhrH6l8l1U3FA5eAIc7i/Df4iSeLQsDb3yUv1PoX4G/3caPiWQ496dk5sYO5vs0IMhgBJ6xizVBHhjSxBDNtAJgAIq4UrgIY9SCu9AFi0hNbxpna1Y3+CBGH5jWylEkkQBDlW3D8rY6Jztqtzj1cbaRziqPcVbyi+NZDVgx0g8ER8qWi7g2oQvOb5m5mOVCPxQwjNFZqNNVOgHQ7OBzmJgZqzgvL0LPqbtpqN7wgWcb2DKcjEePwerpQiLEo3qIyAjqhltCCEXDJFhkSc/ChGL5M1pXUpsP/aQ0y3OZdXns+HegW7ifL6LVdlFCciuQO+xK3bhW3ZZIbYkcCAwEAAQ==");
RSA rsa = RSA.Create();
rsa.ImportSubjectPublicKeyInfo(spkiDer, out _);
// Verify
byte[] hashBytes = Convert.FromBase64String("MDEwDQYJYIZIAWUDBAIBBQAEIGlE+Lr0y76nMOdxNLu1k5g4l3feoWU4MYZm8yo0N5vE");
byte[] signature = Convert.FromBase64String("N3Zc/d8UzODZvlndRhlc6b4uDX2Z6a5okA9al51W2B9hAKjxLygyoZmNkALn/BNyEAWhFQAiC6mFkF0/KmAVF0ci7ZVFlfLkiDSqo4QS67KPHUeDk15aARk/sB5B4yAyG9p4OvhX4gZ+IZNWFb7sqzJ/bdZ39bxxeLGKsChVlsaY4VvROWZRclI2Hox/80znMYfFBU6bQysp34F3+WQo8BE0gXnmwe99y9beyWK099mnjtDf79ZeQ0n34SyYAjf5Hh7xh13VaQ7pXaMkf1gKCXoClbLyKkn/HUgvBZliUhPGI1QTrLGuJSjvPHUA4RNu/jx7XNfp2Sav4yCikL1waOkl3notZBMZyH908wTRAvCvNO7Al453Af22otK5ybgU6HuVqmS6epQXXutUVpCWZKTYrcU+kmQmBqOjAaSRmbzoJw0h8uWJ9mpjdpxpr9U6Y5ri4A8DKt/TyFySA7tRNiuAh8iH737qmsDrpdvbKk661l9ftXWrDqiyVagqfe1/taNFrattQ9wFY4z2bdEHDKZvyLp15/hWZjeH2NvgC0mvuAq+KPgOlXqjgwfq7xS/8OUhYZhkRDvWKR2g3NidXG75RVUHggMIfu0blO/doE0u6cLqYaMprbw4fe+JtUKTkxyWa2Q9tbaPKMc6FepYOw1m2BKM0oDofrmZ5WDud5E=");
bool verified = rsa.VerifyHash(
hashBytes[^32..], // pass H
signature,
HashAlgorithmName.SHA256, // digest is explicitly specified
RSASignaturePadding.Pkcs1);
Console.WriteLine(verified); // True