SignedCms.CheckSignature with SAP certificate is failing - c#

This is a follow-up of this question.
I am writing an external server which gets called by a SAP-Server. The SAP-Server signs the URL with a certificate before it is transmitted. In a previous step the SAP-Server sent the certificate it will be using to sign the URL to my server. So my server has the certificate the SAP-Server is using for signing.
From the SAP-documentation I know the following.
The unsigned URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537
The values of important QueryString-parameters are concatenated (in the same order they appear in the QueryString) to form the "message".
For the given QueryString-Parameters
ContRep = K1
DocId = 361A524A3ECB5459E0000800099245EC
AccessMode = r
AuthId = pawdf054_BCE_26
Expiration = 19981104091537
the generated "message" looks like this:
K1361A524A3ECB5459E0000800099245ECrpawdf054_BCE_2619981104091537
The "message" is used to calculate the hash from which the SecKey is calculated. SAP uses the Digital Signature Standard (DSS) to digitally sign the hash value according to PKCS#. The digital signature is appended to the querystring in a parameter with the name SecKey. The SecKey for the chosen procedure is about 500 bytes long. In the example from the SAP-documentation, the arbitary values 0x83, 0x70, 0x21, 0x42 are chosen for the secKey, for the sake of clarity.
The SecKey is base64 encoded and added to the URL.
0x83, 0x70, 0x21, 0x42 gets to "g3AhQg=="
and the transferred URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537&secKey=g3AhQg%3D%3D
When my server receives the URL I need to check the signature. I recreate the "message" by concatenating the QueryString-parameters the same way as it was described in point 2. (as it is described in the SAP-documentation)
SAP gives this Summary of Technical Information
Format of digital signature: PKCS#7 "signed data"
Public key procedure: DSS
Key length: 512 - 1024 bits
Public exponent: 2^16 + 1
Public key format: X.509 v3 certificate
MD (message digest) algorithm: MD5 or RIPEMD-160
The library for checking signatures can be obtained from SAP AG. Because the standard format PKCS#7 was used for the signature, other products can also be used for decoding.
I receive an "The hash value is not correct"-Exception on line cms.CheckSignature(certificates, true);
private void CheckSignature(string secKey, string message, X509Certificate2 cert)
{
byte[] signature = Convert.FromBase64String(secKey);
ContentInfo ci = new ContentInfo(System.Text.Encoding.ASCII.GetBytes(message));
SignedCms cms = new SignedCms(ci, true);
X509Certificate2Collection certificates = new X509Certificate2Collection(cert);
cms.Decode(signature);
try
{
cms.CheckSignature(certificates, true);
}
catch(Exception ex)
{
log.Error(ex.ToString());
}
}
Can anybody help, or knows what I am doing wrong?

Actually the above function CheckSignature works correct
BUT the second parameter 'message' has to be URL-encoded. Or to be more precise, when concatenating you must use the NOT-URL-DECODED queryString values. [with the same spelling (uppercase/lowercase) SAP uses]
ContRep = AA
DocId = 53730C7E18661EDCB1F816798DAA18B2
AccessMode = r
AuthId = CN=NPL (for concatenating 'CN%3DNPL' is used)
Expiration = 20220511173746
will become the message
AA53730C7E18661EDCB1F816798DAA18B2rCN%3DNPL20220511173746

Related

C# and Kotlin ECDH shared key mismatch

Deriving shared key using:
C# ECDiffieHellmanCng.DeriveKeyMaterial(ECDiffieHellmanPublicKey otherPartyPublicKey)
Kotlin KeyAgreement.generateSecret() followed by KeyAgreement.doPhase(key: Key!, lastPhase: Boolean)
Yields different results using curve "secp384r1".
Kotlin related links point to Kotlin for Android docs due to readability.
Simplified driver code to demonstrate the problem, assuming that C# .NET 7.0.1 console application is "Server" and Kotlin OpenJDK 19.0.1 application is "Client":
C#:
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
var listener = new TcpListener(IPAddress.Any, 13000);
listener.Start();
using var client = await listener.AcceptTcpClientAsync();
var sharedKey = await GetSharedKey(client, CancellationToken.None);
async Task<byte[]> GetSharedKey(TcpClient client, CancellationToken token)
{
//Generate ECDH key pair using secp384r1 curve
var ecdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"));
var publicKeyBytes = ecdh.ExportSubjectPublicKeyInfo();
Console.WriteLine($"Server Public Key: {Convert.ToBase64String(publicKeyBytes)}, " +
$"Length: {publicKeyBytes.Length}");
//Send the generated public key encoded in X.509 to client.
var stream = client.GetStream();
await stream.WriteAsync(publicKeyBytes, token);
//Receive client's public key bytes (X.509 encoding).
var otherPublicKeyBytes = new byte[publicKeyBytes.Length];
await stream.ReadExactlyAsync(otherPublicKeyBytes, 0, otherPublicKeyBytes.Length, token);
//Decode client's public key bytes.
var otherEcdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"));
otherEcdh.ImportSubjectPublicKeyInfo(otherPublicKeyBytes, out _);
Console.WriteLine($"Client Public Key: {Convert.ToBase64String(otherEcdh.ExportSubjectPublicKeyInfo())}, " +
$"Length: {otherEcdh.ExportSubjectPublicKeyInfo().Length}");
//Derive shared key.
var sharedKey = ecdh.DeriveKeyMaterial(otherEcdh.PublicKey);
Console.WriteLine($"Shared key: {Convert.ToBase64String(sharedKey)}, " +
$"Length: {sharedKey.Length}");
return sharedKey;
}
Kotlin:
import java.net.Socket
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.spec.ECGenParameterSpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.KeyAgreement
fun main(args: Array<String>) {
val socket = Socket("127.0.0.1", 13000)
val sharedKey = getSharedKey(socket)
}
private fun getSharedKey(socket: Socket): ByteArray {
//Generate ECDH key pair using secp384r1 curve
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(ECGenParameterSpec("secp384r1"))
val keyPair = keyGen.generateKeyPair()
println("Client Public Key: ${Base64.getEncoder().encodeToString(keyPair.public.encoded)}, Length: ${keyPair.public.encoded.size}")
//Receive server's public key bytes (encoded in X.509)
val input = socket.getInputStream()
val publicKeyBytes = input.readNBytes(keyPair.public.encoded.size)
//Send the generated public key encoded in X.509 to server
val output = socket.getOutputStream()
output.write(keyPair.public.encoded)
// Decode the server's public key
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("EC")
val otherPublicKey = keyFactory.generatePublic(keySpec)
println("Server Public Key: ${Base64.getEncoder().encodeToString(otherPublicKey.encoded)}, Length: ${otherPublicKey.encoded.size}")
// Use KeyAgreement to generate the shared key
val keyAgreement = KeyAgreement.getInstance("ECDH")
keyAgreement.init(keyPair.private)
keyAgreement.doPhase(otherPublicKey, true)
val sharedKey = keyAgreement.generateSecret()
println("Shared key: ${Base64.getEncoder().encodeToString(sharedKey)}, Length: ${sharedKey.size}")
return sharedKey
}
C# output:
Server Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEqza/eiK23hQIEW5mVdqOc0hAP3tPqittlcvPa6bGdyJK9n64sg0qYyDoPsxJ4pf7ROLz0ACrDS7n/e5Z0J1SMsWpBDViS8NRBvKwa1rQjWdFR0wzRaeVg09LIjnGs4Mj, Length: 120
Client Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE30zvqkljT4STiE6XfLtoN147WRGA92rz9BLZfbRkOjz7uNbQ3az46DdoyQi6+eON7QVjIf2H5LKBANSk+C5zRX6u8jjrbhURDHYBKgijOddy6mOaEwiADijD/NX72O2L, Length: 120
Shared key: /u+tZYHar4MxXfrn2oqPZAqhiB2pkSTRBZ12rUxdnII=, Length: 32
Kotlin output:
Client Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE30zvqkljT4STiE6XfLtoN147WRGA92rz9BLZfbRkOjz7uNbQ3az46DdoyQi6+eON7QVjIf2H5LKBANSk+C5zRX6u8jjrbhURDHYBKgijOddy6mOaEwiADijD/NX72O2L, Length: 120
Server Public Key: MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEqza/eiK23hQIEW5mVdqOc0hAP3tPqittlcvPa6bGdyJK9n64sg0qYyDoPsxJ4pf7ROLz0ACrDS7n/e5Z0J1SMsWpBDViS8NRBvKwa1rQjWdFR0wzRaeVg09LIjnGs4Mj, Length: 120
Shared key: lErK9DJAutaJ4af7EYWvtEXicAwfSuadtQhlZxug26wGkgB/ce7hF6ihLL87Sqc3, Length: 48
It seems there are no problems with public key import/export, but C# side fails to even produce key of correct length (384 / 8 = 48).
Edit:
Somebody noticed that curiously enough C# "shared key" is Kotlin's shared key's SHA256 hash instead of the actual key.
I strongly suspect it's because of default key derivation function mismatch, but am not completely sure.
I would like to know what am I doing wrong and how to fix the issue.
Edit#2 - Solution:
As the accepted answer suggests - my suspicion is not entirely wrong. ECDiffieHellmanCng.DeriveKeyMaterial does a bit extra unnecessary work - namely returning derived key's SHA256 hash (by default) instead of the actual key and does not provide any means of returning the actual key.
For anyone that is interested in getting 48 byte shared key you will have to be content with it's SHA384 (or some other hashing algorithm) hash instead (or use BouncyCastle):
C# changes:
//Generate ECDH key pair using secp384r1 curve and change default key's hashing algorithm SHA256 to SHA384
var ecdh = new ECDiffieHellmanCng(ECCurve.CreateFromFriendlyName("secp384r1"))
{
HashAlgorithm = CngAlgorithm.Sha384
};
Kotlin changes:
val sharedKey = keyAgreement.generateSecret()
val sharedKeyHash = MessageDigest.getInstance("SHA384").digest(sharedKey)
println("Shared key SHA384 hash: ${Base64.getEncoder().encodeToString(sharedKeyHash)}, Length: ${sharedKeyHash.size}")
return sharedKeyHash
I also suggest to rename the GetSharedKey method to what it actually is - GetSharedKeysSHA384Hash.
The problem is that C# does do more than what is expected from the class. I.e. as usual, the .NET library doesn't adhere to the principle of least surprise:
The ECDiffieHellmanCng class enables two parties to exchange private key material even if they are communicating through a public channel. Both parties can calculate the same secret value, which is referred to as the secret agreement in the managed Diffie-Hellman classes. The secret agreement can then be used for a variety of purposes, including as a symmetric key. However, instead of exposing the secret agreement directly, the ECDiffieHellmanCng class does some post-processing on the agreement before providing the value. This post processing is referred to as the key derivation function (KDF); you can select which KDF you want to use and set its parameters through a set of properties on the instance of the Diffie-Hellman object.
Of course, the tw... developers that created the code don't exactly specify on what they perform the KDF, nor do they specify the default method used from the options that are shown. However, you can expect that they perform it on the X coordinate that is calculated by the Diffie-Hellman key agreement.
That said, it is not very clear from the Java class description either. The standard names document references RFC 3278, which points to the old Sec1 standard, section 6.1 using a broken link. Now Sec1 can still be downloaded, and if we look at section 6.1 we find a construction to encode integers to the a number of bytes that is the field size (and then take the required bytes). What however is returned is undoubtedly the same encoded X-coordinate that is the Input Keying Material to the KDF that Microsoft uses.
Phew, that was a lot of words to say that you have to take the result of the Kotlin code in bytes and then perform the SHA-256 algorithm on it. Oh, yeah, the SHA-256 default was guessed, it's also not specified as far as I can see by Microsoft, although they do expose the KeyDerivationFunction and HashAlgorithm properties and define the defaults for them.
There are some options to choose the parameters for the various KDF functions for ECDiffieHellmanCng, but you seem to be out of luck if you want to have the "raw" X-coordinate. If you want that you may need to use Bouncy Castle for C# but beware that it returns a raw integer for the X-coordinate instead of an encoding of a statically sized, unsigned, big endian integer.

Signing email in C#

Based on this question I decided to sign emails send from ASP.NET.MVC to decrease SPAM score of emails, but I have some bug somewhere.
Code:
public void SendEmail(MailMessage mailMessage)
{
string domain = "kup-nemovitost.cz";
var message = MimeMessage.CreateFromMailMessage(mailMessage);
HeaderId[] headers = new HeaderId[] { HeaderId.From, HeaderId.Subject, HeaderId.Date };
DkimCanonicalizationAlgorithm headerAlgorithm = DkimCanonicalizationAlgorithm.Relaxed;
DkimCanonicalizationAlgorithm bodyAlgorithm = DkimCanonicalizationAlgorithm.Relaxed;
string dkimPath = Path.Combine(ConfigHelper.GetDataPath(), "DKIM");
string privateKey = Path.Combine(dkimPath, "kup-nemovitost.cz.private.rsa");
DkimSigner signer = new DkimSigner(privateKey, domain, "mail")
{
SignatureAlgorithm = DkimSignatureAlgorithm.RsaSha1,
AgentOrUserIdentifier = "#" + domain,
QueryMethod = "dns/txt",
};
message.Prepare(EncodingConstraint.SevenBit);
message.Sign(signer, headers, headerAlgorithm, bodyAlgorithm);
using (var client = new MailKit.Net.Smtp.SmtpClient())
{
client.Connect("localhost", 25, false);
client.Send(message);
client.Disconnect(true);
}
}
I check the result on http://www.isnotspam.com, the output is following:
DKIM check details:
----------------------------------------------------------
Result: invalid
ID(s) verified: header.From=no-reply#kup-nemovitost.cz
Selector=mail
domain=kup-nemovitost.cz
DomainKeys DNS Record=mail._domainkey.kup-nemovitost.cz
My DNS record is:
# IN TXT "v=dkim1; s=mail; p=migfma0gcsqgsib3dqebaquaa4gnadcbiqkbgqdnov2pxnjmghdpxw5wpypk1rf7 kxs+5ouvh6f0hraryncku6wbvq+xovbgxz1kuddcb/s9o8wquftxrlffniik3wbm qc+upm+ndloxcxwy0bb2iktbgnmndjiexm/z0npaviwzebr2k6vqdzbp+lmcuece bwasqgw2fki5ospb4qidaqab"
UPDATE:
I fix some issues in DNS record and I have found better online checker at dkimcore.org
I still face validation issue of my public key. I generated 1024 RSA using puttyGen (ppk) and convert it to RSA format. The original file from PuttyGen is:
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "rsa-key-20170606"
AAAAB3NzaC1yc2EAAAABJQAAAIEAiyEwx+Idlf/Qp2fTYrQMwV3MuF9W7yaKDMHk
hzoH+MqWKtNDngQoJcmbyrkMeF0VLYo246ma3gPZh9cDL7i8ygOYKagbyUjgtZFz
y+et0tY/+G/IZNaHiQp0QuG/J71uZrl4Jlgkq+0s5bZxpRR45aRpcG1HQMIm6Ku7
lgmOt88=
---- END SSH2 PUBLIC KEY ----
So I just copy the content (except the commented lines) to DNS record and I got following output from checker:
p= AAAAB3NzaC1yc2EAAAABJQAAAIEAiyEwx+Idlf/Qp2fTYrQMwV3MuF9W7yaKDMHkhzoH+MqWKtNDngQoJcmbyrkMeF0VLYo246ma3gPZh9cDL7i8ygOYKagbyUjgtZFzy+et0tY/+G/IZNaHiQp0QuG/J71uZrl4Jlgkq+0s5bZxpRR45aRpcG1HQMIm6Ku7lgmOt88=
This doesn't seem to be a valid RSA public key: RSA.xs:178: OpenSSL error: wrong tag at blib/lib/Crypt/OpenSSL/RSA.pm (autosplit into blib/lib/auto/Crypt/OpenSSL/RSA/new_public_key.al) line 91.
As we figured out in comments - your code is fine, but you have problems with public key. First, it has spaces in dns txt record, which it cannot have. Then, you have wrong format for a public key (SSH2). Tools that verify signature expect different format (regular PEM RSA), which should look like this:
--- BEGIN RSA PUBLIC KEY ---
MII ....
--- END RSA PUBLIC KEY ---
So, the same as your private key. You can convert SSH2 key to PEM with
ssh-keygen -f output.pub -m 'PEM' -e > public.pem
Where output.pub is public key in SSH2 format. With such key (and without spaces in DNS record) it should work fine, assuming you put that into right dns record - key_selector._domainkey.your.domain.

iTextSharp read timestamp certificate

would sameone mind helping me with code using iTextSharp?
I have signed pdf file and I need to retrieve information about signature and timestamp.
I have no problem with getting signing certificate's information. But I cannot obtain information from TSA certificate. I only get information about issuer and I need to get information about date not before and after too and other.
Here is my code:
PdfReader reader = new PdfReader(file);
AcroFields af = reader.AcroFields;
List<string> names = af.GetSignatureNames();
for (int i = 0; i < names.Count; ++i)
{
// it is working fine
string name = (string)names[i];
iTextSharp.text.pdf.security.PdfPKCS7 pk = af.VerifySignature(name);
Console.WriteLine();
Console.WriteLine(String.Format("Podepsal: {0}", pk.SignName));
Console.WriteLine(String.Format("Datum: {0}", pk.SignDate));
Console.WriteLine(String.Format("Platnost od: {0}", pk.SigningCertificate.NotBefore));
Console.WriteLine(String.Format("Platnost do: {0}", pk.SigningCertificate.NotAfter));
// here I need to help
Org.BouncyCastle.Tsp.TimeStampToken tts = pk.TimeStampToken;
string s = tts.TimeStampInfo.Tsa.Name.ToString();
// this line returns null
DateTime dt = tts.SignerID.Certificate.NotAfter;
}
Here is pdf sample http://www.filedropper.com/sample
Thank you!
In contrast to the basic signature which has already been verified by iText during your af.VerifySignature call, the signature time stamp has not yet been analyzed. In particular the actual TSA certificate has not yet been looked for.
Thus, you first have to identify the certificate in question. Usually it is included in the certificate collection a time stamp brings along, so in the following I assume it is there (it is in case of your sample file). We look it up by issuer and serial number, and then we verify the time stamp with it to be sure it is the right certificate, not a fake. Thereafter you can inspect the certificate as you like.
// Define a selector matching issuer and serial number
X509CertStoreSelector selector = new X509CertStoreSelector();
selector.Issuer = tts.SignerID.Issuer;
selector.SerialNumber = tts.SignerID.SerialNumber;
// Retrieve the matching certificates from the time stamp certificate collection
System.Collections.ICollection certs = tts.GetCertificates("COLLECTION").GetMatches(selector);
// Assuming at most one match, retrieve this matching certificate
IEnumerator enumCerts = certs.GetEnumerator();
if (enumCerts.MoveNext())
{
X509Certificate cert = (X509Certificate)enumCerts.Current;
// Verify that this is the correct certificate by verifying the time stamp token
tts.Validate(cert);
// Extracting information from the now verified tsa certificate
Console.WriteLine(String.Format("Not before: {0}", cert.NotBefore));
Console.WriteLine(String.Format("Not after: {0}", cert.NotAfter));
}

.NET RSAPKCS1KeyExchangeFormatter Class - Exception "The parameter is incorrect"

I tried to split up the sample for .NET RSAPKCS1KeyExchangeFormatter Class from https://msdn.microsoft.com/EN-US/library/8kkwbeez(v=VS.110,d=hv.2).aspx into 2 console apps (Alice, Bob) using a common class named KeyExchange. This class contains two methods:
GenerateEncryptedSessionKeyAndIV: runs on Alice, encrypts the session key, and for test purposes decrypts it.
ProcessEncryptedSessionKeyAndIV: runs on Bob, fails to decrypt the session key with Exception "The parameter is incorrect". Although the byte arrays look correct. Please help.
public KeyExchange()
{
rsaKey = new RSACryptoServiceProvider(); // asymmetric encryption/decryption
aes = new AesCryptoServiceProvider(); // symmetric encryption/decryption
}
public byte[] PublicKey
{
get { return rsaKey.ExportCspBlob(false); } // used by partner who wants to send secret session key
set { rsaKey.ImportCspBlob(value); } // used by partner who receives secret session key
}
public void GenerateEncryptedSessionKeyAndIV(out byte[] iv, out byte[] encryptedSessionKey)
{
iv = aes.IV; // Gets the initialization vector (IV) for the symmetric algorithm.
// Encrypt the session key
RSAPKCS1KeyExchangeFormatter keyFormatter = new RSAPKCS1KeyExchangeFormatter(rsaKey); // Initializes a new instance of the RSAPKCS1KeyExchangeFormatter class with the specified key.
encryptedSessionKey = keyFormatter.CreateKeyExchange(aes.Key, typeof(Aes)); // Create and return the encrypted key exchange data
// test only: the next 2 lines are to prove that the secret key can be obtained from the the encrypted key exchange data here on Alice,
// the same code failes executed on Bob (see method ProcessEncryptedSessionKeyAndIV)
RSAPKCS1KeyExchangeDeformatter keyDeformatter = new RSAPKCS1KeyExchangeDeformatter(rsaKey);
byte[] helper = keyDeformatter.DecryptKeyExchange(encryptedSessionKey);
}
public void ProcessEncryptedSessionKeyAndIV(byte[] iv, byte[] encryptedSessionKey)
{
aes.IV = iv; // Sets the initialization vector (IV) for the symmetric algorithm.
// Decrypt the session key, Create a KeyExchangeDeformatter
RSAPKCS1KeyExchangeDeformatter keyDeformatter = new RSAPKCS1KeyExchangeDeformatter(rsaKey);
// obtain the secret key (32 bytes) from from the encrypted key exchange data (128 bytes)
aes.Key = keyDeformatter.DecryptKeyExchange(encryptedSessionKey); // this results in CryptographicException: The parameter is incorrect.
}
Okay, psychic debugging time.
You have Alice construct one of these and call GenerateEncryptedSessionKeyAndIV(). She sends that value, and the value of PublicKey (which shouldn't be a property because it does a lot more work than you want happening in a debugger every time you hit F10).
You have Bob construct one of these and assign PublicKey, then call ProcessEncryptedSessionKeyAndIV.
The exception is because Bob doesn't have the private key, so he can't decrypt.
You're doing KeyExchange, which suggests you're online, which suggests you should just use TLS and call it a day. If you're offline you want KeyAgreement (Diffie-Hellman, or EC Diffie-Hellman).
Nevertheless, the right way is
Private-key-holder sends their public key, preferably as a certificate
Other party verifies the public key (which is much easier when it's a certificate... nigh on impossible if it's just key data)
Other party generates some data to hide
Other party encrypts the data using the received public key
Other party sends the encrypted data back
Private-key-holder decrypts the data (with the private key)
Now both sides know what the data was (which could be a key, could be a key+algorithm, could be an input into a KDF, ...)
For KeyExchange these roles are usually called Server (private-key-holder) and Client (other party).

PayPal webhook signature verification in C#

I have a C# application that receives webhook notifications from PayPal and I want to verify the signature as described in PayPal docs:
https://developer.paypal.com/docs/integration/direct/rest-webhooks-overview/#event-types
The code snippet in the docs is for Java, not C#. The first thing I don't know is in which format the CRC32 should be appended (decimal, hex, ???). I have tried several variants and I have following code so far, always with VerifyData() returning false:
string transmissionSig = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-SIG"];
string transmissionId = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-ID"];
string transmissionTime = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-TIME"];
string signatureAlgorithm = HttpContext.Request.Headers["PAYPAL-AUTH-ALGO"]; //signatureAlgorithm == "SHA256withRSA"
string certUrl = HttpContext.Request.Headers["PAYPAL-CERT-URL"];
uint crc = calculateCrc32(eventBody);
string expectedSignature = String.Format("{0}|{1}|{2}|{3}", transmissionId, transmissionTime, webhookId, crc);
string certData = new System.Net.WebClient().DownloadString(certUrl);
X509Certificate2 cert = new X509Certificate2(getBytes(certData));
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)cert.PublicKey.Key;
byte[] signature = Convert.FromBase64String(transmissionSig);
byte[] expectedBytes = getBytes(expectedSignature);
bool verified = rsa.VerifyData(expectedBytes, CryptoConfig.MapNameToOID("SHA1"), signature);
What am I doing wrong?
UPDATE
I use this class for CRC calculation: https://github.com/damieng/DamienGKit/blob/master/CSharp/DamienG.Library/Security/Cryptography/Crc32.cs
Example eventBody (from webhook simulator):
{"id":"WH-2WR32451HC0233532-67976317FL4543714","create_time":"2014-10-23T17:23:52Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"A successful sale payment was made for $ 0.48 USD","resource":{"id":"80021663DE681814L","create_time":"2014-10-23T17:22:56Z","update_time":"2014-10-23T17:23:04Z","amount":{"total":"0.48","currency":"USD"},"payment_mode":"ECHECK","state":"completed","protection_eligibility":"ELIGIBLE","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","clearing_time":"2014-10-30T07:00:00Z","parent_payment":"PAY-1PA12106FU478450MKRETS4A","links":[{"href":"https://api.paypal.com/v1/payments/sale/80021663DE681814L","rel":"self","method":"GET"},{"href":"https://api.paypal.com/v1/payments/sale/80021663DE681814L/refund","rel":"refund","method":"POST"},{"href":"https://api.paypal.com/v1/payments/payment/PAY-1PA12106FU478450MKRETS4A","rel":"parent_payment","method":"GET"}]},"links":[{"href":"https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714","rel":"self","method":"GET"},{"href":"https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714/resend","rel":"resend","method":"POST"}]}
And it's CRC that I'm getting and appending to expectedSignature: 3561502039
you should get algorithm from the header in stead of hard-coding it. SHA256 is the currently supported algorithm I think.

Categories

Resources