ASP.NET: Sign URL with public and private keys from files - asp.net

I want to update Google AMP cache, so I need to sign an URL as described here.
My main issue: I'm struggling massively with how I should get my certificates/keys and how to include them in my code below.
I just can't find any all covering instructions for Windows and IIS.
I have been reading these posts:
Using /update-cache requests to update AMP pages
How can I sign a file using RSA and SHA256 with .NET?
I don't want to use my computer's certificate store as described in the second post. I'd rather use files on disk for both public and private keys.
From my production server IIS, I exported my certificate to a .pfx file, from which I then extracted the private key and certificate using the instructions on the bottom of this site.
The server.key contains -----BEGIN RSA PRIVATE KEY-----, which If I use that to load into the privateCert variable in the code below throws error Cannot find the requested object.
What I have gotten from my SSL provider:
www_example_com.crt, www_example_com.p7b, the certificate code (see below).
Steps I've taken:
I created test-public-key.cer by opening www_example.com.crt and using the Copy to file wizard to copy it to a base64 encoded .cer file.
I saved certificate code I received from my SSL provider as file test-private-key.cer.
When I run the following code I get error
Object reference not set to an instance of an object.
on line key.FromXmlString(privateCert.PrivateKey.ToXmlString(True))
Dim publicCert As X509Certificate2 = New X509Certificate2("C:\temp\_certificates\test-public-key.cer")
Dim privateCert As X509Certificate2 = New X509Certificate2("C:\temp\_certificates\test-private-key.cer")
'Round-trip the key to XML and back, there might be a better way but this works
Dim key As RSACryptoServiceProvider = New RSACryptoServiceProvider
key.FromXmlString(privateCert.PrivateKey.ToXmlString(True))
'Create some data to sign
Dim data() As Byte = System.Text.Encoding.Unicode.GetBytes(signatureUrl)
'Sign the data
Dim sig() As Byte = key.SignData(data, CryptoConfig.MapNameToOID("SHA256"))
Dim AMPURLSignature As String = EncodeTo64(sig.ToString)
'Lastly, the verification can be done directly with the certificate's public key without need for the reconstruction as we did with the private key:
key = CType(publicCert.PublicKey.Key, RSACryptoServiceProvider)
If Not key.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), sig) Then
Throw New CryptographicException
End If
EncodeTo64 function
Public Shared Function EncodeTo64(ByVal toEncode As String) As String
Dim toEncodeAsBytes As Byte() = System.Text.ASCIIEncoding.ASCII.GetBytes(toEncode)
Dim returnValue As String = System.Convert.ToBase64String(toEncodeAsBytes)
Return returnValue
End Function
certificate code
-----BEGIN CERTIFICATE-----
MIIF (...) DXuJ
-----END CERTIFICATE-----
UPDATE 1
I was able to generate a mysite.pfx file by following the export steps on this page.
In the wizard I made sure to select "Yes, export the private key" and I added a password. The rest of the steps I followed verbatim.
I then also ran these commands:
openssl pkcs12 -in mysite.pfx -nocerts -out private-key-VPS.pem
penssl pkcs12 -in mysite.pfx -clcerts -nokeys -out certificate-VPS.pem
I ended up with private-key-VPS.pem and a certificate-VPS.pem files
I'm aware the steps to get the mysite.pfx are slightly different than what #CodeFuller described, but so far so good?
I then added code:
Dim certificate As X509Certificate2 = New X509Certificate2("C:\temp\_certificates\prodserverV2\mysite.pfx", "mypwd")
Dim rsa As RSA = certificate.GetRSAPrivateKey()
Dim data() As Byte = System.Text.Encoding.Unicode.GetBytes(signatureUrl)
Dim sig() As Byte = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
Dim AMPURLSignature As String = EncodeTo64(sig.ToString)
But there I get 4 errors:
GetRSAPrivateKey' is not a member of 'X509Certificate2'.
'SignData' is not a member of 'RSA'.
'HashAlgorithmName' is not declared. It may be inaccessible due to its protection level.
'RSASignaturePadding' is not declared. It may be inaccessible due to its protection level.
UPDATE 2
Thanks #CodeFuller! I targeted framework 4.6.1 and now seem to be 1 step further. I end up with an URL like this: https://www-mysite-com.cdn.ampproject.org/update-cache/c/s/www.mysite.com/articles/270/newarticle1/amp?amp_action=flush&_ts=1522939248&_url_signature=U30zdGVtLkJ5uGVbRQ==. How can I now check if it's a valid URL?
I'm checking section "Generate the RSA key" on this page, but I'm confused, since I actually already just coded these steps or not? How can I check whether the URL I now end up with is valid?
UPDATE 3
Ok, I tried your new code. Still get the URL signature verification error. I tried with both the /amp URL of my article and without the /amp part in my URL. Both result in the same URL signature verification error.
I noticed when I print the final URL to my website (see code below), the URL reads:
https://www-toptrouwen-nl.cdn.ampproject.org/update-cache/c/s/www.toptrouwen.nl/artikelen/132/het-uitwisselen-van-de-trouwringen-hoe-voorkom-je-bloopers/amp?amp_action=flush&_ts=1523094395&_url_signature=U3lzdGVrLkn5dGVbXQ==
Notice that where the parameters should be amp_ts and amp_url_signature, they now are _ts and _url_signature respectively.
I tried editing the URL before I do the call to Google by manually renaming parameters _ts and _url_signature to amp_ts and amp_url_signature. But I guess that would result in a difference between the signature and the actual URL. Could it be that somehow my code botches the & character and therefore when I rename these manually later it always result in a signature verification? Do you see what I could fix in my code?
BTW: I tried replacing & with %26 in code-behind before signing the URL but then I get a Google 404 error:
The requested URL /update-cache/c/s/www.toptrouwen.nl/artikelen/132/het-uitwisselen-van-de-trouwringen-hoe-voorkom-je-bloopers/amp?amp_action=flush%26amp_ts=1523094395%26amp_url_signature=U3lzdGVrLkJ1dGVbXQ== was not found on this server. That’s all we know.
My code:
Dim ampBaseUrl As String = "https://www-toptrouwen-nl.cdn.ampproject.org"
Dim signatureUrl As String = "/update-cache/c/s/www.toptrouwen.nl/artikelen/132/het-uitwisselen-van-de-trouwringen-hoe-voorkom-je-bloopers/amp?amp_action=flush&amp_ts=" + tStamp
Dim rsa As RSA = certificate.GetRSAPrivateKey()
Dim data() As Byte = System.Text.Encoding.ASCII.GetBytes(signatureUrl)
Dim sig() As Byte = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
Dim AMPURLSignature As String = EncodeTo64(sig.ToString)
Dim url As String = ampBaseUrl + signatureUrl + "&amp_url_signature=" + AMPURLSignature
ltStatus.Text = "AMP URL:<a target='_blank' href='" + url + "'>" + url + "</a>"
Also, I'm sure this page exists in Google AMP cache, since I can see and request it in Google's search results on my mobile device.
UPDATE 4
I'm getting close I think and also getting some extra help, see here: https://github.com/ampproject/amphtml/issues/14483#issuecomment-380549060
What I'm trying now to make it easier for others to test as well: Instead of depending on my SSL I now ran the following commands to get a public and private key
openssl genrsa 2048 > private-key.pem
openssl rsa -in private-key.pem -pubout >public-key.pem
I now have files private-key.pem and public-key.pem
I'll rename public-key.pem to apikey.pub and place that on https://example.com/.well-known/amphtml/apikey.pub
I want to take the easiest approach recommended by #CodeFuller and create a .pfx file that I can then load into a variable of type X509Certificate2.
When I run this command:
openssl pkcs12 -export -out keys.pfx -inkey private-key.pem -in public-key.pem
I get the error: unable to load certificates
But this time I don't have a .crt file, only a public-key.pem. How can I get a .pfx file? I already checked here.

I saved certificate code I received from my SSL provider as file
test-private-key.cer
...
certificate code
-----BEGIN CERTIFICATE-----
MIIF (...) DXuJ
-----END CERTIFICATE-----
The file stored in a format
-----BEGIN CERTIFICATE-----
MIIF (...) DXuJ
-----END CERTIFICATE-----
is a certificate which basically contains a public key. It does not contain private key. That's why when you create instance of X509Certificate2 from such file, it's HasPrivateKey property is set to False and PrivateKey returns Nothing, and following statement expectedly throws NullReferenceException:
privateCert.PrivateKey.ToXmlString(True)
In order to sign the data, you need a private key. Private keys have the following format
-----BEGIN RSA PRIVATE KEY-----
MIICXQ...B7Bou+
-----END RSA PRIVATE KEY-----
Such private keys are usually stored in *.key or *.pem (Privacy Enhanced Mail) files. There is no built-in way to load instance of X509Certificate2 from pem file. There are a lot of code samples available how to do it, you will find them in the question linked above. However the easiest solution will be to create pfx file (containing both private and public keys). Then you could easily load pfx with corresponding constructor of X509Certificate2.
Creation of pfx file is very easy with SSL tool. If private.key contains private key (-----BEGIN RSA PRIVATE KEY-----) and public.crt contains public key (-----BEGIN CERTIFICATE-----), they you could create pfx file with the following command:
openssl pkcs12 -export -out keys.pfx -inkey private.key -in
public.crt
You will be asked to enter the password. This password will also be used when you load the key to X509Certificate2:
Dim certificate As X509Certificate2 = New X509Certificate2("d:\CodeFuller\_days\2018.04.05\keys.pfx", "Password here")
Now HasPrivateKey property is set to True and PrivateKey returns the instance of RSACryptoServiceProvider.
UPDATE
Regarding this code:
'Round-trip the key to XML and back, there might be a better way but this works
Dim key As RSACryptoServiceProvider = New
RSACryptoServiceProvider
key.FromXmlString(privateCert.PrivateKey.ToXmlString(True))
The instance of RSACryptoServiceProvider is actually stored in certificate.PrivateKey so you could avoid above code and replace it with:
Dim provider As RSACryptoServiceProvider = certificate.PrivateKey
However your current SignData() call will not work:
Dim sig() As Byte = key.SignData(data, CryptoConfig.MapNameToOID("SHA256"))
This will throw following exception:
System.Security.Cryptography.CryptographicException: 'Invalid
algorithm specified.'
The root cause is that RSACryptoServiceProvider does not support SHA256. That's why I suggest replacing it with RSACng in the following way:
Dim rsa As RSA = certificate.GetRSAPrivateKey()
Dim data() As Byte = System.Text.Encoding.Unicode.GetBytes(signatureUrl)
Dim sig() As Byte = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
Method GetRSAPrivateKey was added to X509Certificate2 class only since .NET Framework 4.6, so consider upgrading if you get following error:
error BC30456: 'GetRSAPrivateKey' is not a member of
'X509Certificate2'
UPDATE 2 (regarding URL validation)
The page you referenced contains openssl command for verifying the signature:
openssl dgst -sha256 -signature signature.bin -verify public-key.pem url.txt
However in your case it will be just a sanity check, because you have just generated the signature with a valid procedure. So answering your question:
How can I check whether the URL I now end up with is valid?
The best check is just to send request to AMP Cache with signed URL and check the response. I haven't used AMP Cache before but I believe it will respond with some HTTP error if the signature is invalid.
UPDATE 3 (regarding failed signature verification)
Update AMP Content page contains following command line for signing the URL:
echo -n >url.txt
'/update-cache/c/s/example.com/article?amp_action=flush&amp_ts=1484941817'
cat url.txt | openssl dgst -sha256 -sign private-key.pem >signature.bin
I have compared result signature built by this command with the signature calculated by the code from my answer. It turned out that they differ. I have researched the possible root cause and found that the problem is caused by the way we get URL bytes. Currently it's:
Dim data() As Byte = System.Text.Encoding.Unicode.GetBytes(signatureUrl)
However we should sign the URL represented in ASCII. So replace above line with:
Dim data() As Byte = System.Text.Encoding.ASCII.GetBytes(signatureUrl)
Now both signatures, from openssl utility and the code above, matches. If after the fix you still get URL signature verification error from Google AMP, then the problem will be with the input URL passed for signing.
UPDATE 4 (Getting PFX from private and public keys)
Generate private key:
openssl genrsa 2048 > private-key.pem
Generate public key:
openssl rsa -in private-key.pem -pubout > public-key.pem
Create certificate signing request:
openssl req -new -key private-key.pem -out certificate.csr
Create certificate:
openssl x509 -req -days 365 -in certificate.csr -signkey private-key.pem -out public.crt
You will be asked here for some certificate fields, e.g. Country Name, Organization Name, etc. It does not really matter which values you use, since you need this certificate for test purposes.
Create pfx file:
openssl pkcs12 -export -out keys.pfx -inkey private-key.pem -in public.crt

This is the code I use for signing a string with a certificate from disk. I modified it for use in an url. Maybe it will be of help to you.
public string signString(string originalString)
{
//load the certificate from disk
X509Certificate2 cert = loadCertificateFromFile("myCert.pfx", "myPassword");
//get the associated CSP and private key
using (RSACryptoServiceProvider csp = (RSACryptoServiceProvider)cert.PrivateKey)
using (SHA1Managed sha1 = new SHA1Managed())
{
//hash the data
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] data = encoding.GetBytes(originalString);
byte[] hash = sha1.ComputeHash(data);
//sign the hash
byte[] signed = csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
//convert to base64 and encode for use in an URL
return Server.UrlEncode(Convert.ToBase64String(signed));
//or return a regular string
//return Encoding.Default.GetString(signed);
}
}
public bool verifyString(string originalString, string signedString)
{
//convert back from base64 and url encoded string
byte[] signature = Convert.FromBase64String(Server.UrlDecode(signedString));
//or a regular string
//byte[] signature = Encoding.Default.GetBytes(signedString);
//load the certificate from disk
X509Certificate2 cert = loadCertificateFromFile("myCert.pfx", "myPassword");
//get the associated CSP and private key
using (RSACryptoServiceProvider csp = (RSACryptoServiceProvider)cert.PrivateKey)
using (SHA1Managed sha1 = new SHA1Managed())
{
//hash the data
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] data = encoding.GetBytes(originalString);
byte[] hash = sha1.ComputeHash(data);
//sign the hash
byte[] signed = csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
//verify the signature
return csp.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), signature);
}
}
public X509Certificate2 loadCertificateFromFile(string path, string password)
{
//get the absolute path
string absolutePath = Server.MapPath(path);
//for non-website users, use this
//string absolutePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
if (!string.IsNullOrEmpty(password))
{
return new X509Certificate2(absolutePath, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
}
else
{
return new X509Certificate2(absolutePath);
}
}
Usage:
string originalString = "This is a test string";
string signedString = signString(originalString);
bool stringIsCorrect = verifyString(originalString, signedString);
I now see you use VB. You can use this to convert the code http://www.carlosag.net/tools/codetranslator. However it may need some modification afterwards as I don't know the accuracy of the converter.

Related

Converting Openssl signing to .NET6

Application invokes openssl for signing using
openssl rsautl -sign -in rasi.bin -inkey riktest.key -out allkiri.bin
How to convert this to .NET 6 so that invoking openssl is not required?
riktest.key is text file containing
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAfddAQEArYjDH7msMFifeYc1AG/TkKpcz2LITI73sC0eqnlgmWi3F7PD
Bo8lWrCw32h3v/FFMrK8KuktlnBtsSLaCCz1DWuXORzHaW7EqG8O8QNzFSmhIoqp
...
This is ASP.NET 6 MVC application. Does .NET 6 System.Security.Cryptography namespace contain this OpenSsl functionality ?
Why generally the native .NET methods cannot be used
For RSASignaturePadding.Pkcs1, the native .NET implementations SignData() and SignHash() follow the RSASSA-PKCS1-v1_5 signature scheme described in RFC8017, which applies EMSA-PKCS1-v1_5 as encoding operation: The message is hashed and the following value is signed (i.e. encrypted with the private key):
EM = 0x00 || 0x01 || PS || 0x00 || T
Here PS consists of so many 0xff values that the size of EM is equal to the size of the modulus of the key. T is the DER encoding of the DigestInfo value, which contains the digest OID and the hash, e.g. for SHA256:
3031300d060960864801650304020105000420 || H
where H is the 32 bytes SHA56 hash of the message M to be signed.
In contrast, openssl rsautl uses the RSA algorithm directly, as mentioned in the NOTES section, i.e. the following data is signed:
EM' = 0x00 || 0x01 || PS || 0x00 || M
This cannot be achieved with the native .NET methods in general (except for a special use case, see below): SignData() hashes and therefore fails, SignHash() does not hash but internally (like SignData()) generates the DER encoding of the DigestInfo value.
An alternative is BouncyCastle, which signs with the algorithm NoneWithRSA just like openssl rsautl.
One disadvantage of this algorithm is that only short messages can be signed due to the missing hashing, since the length criterion cannot be fulfilled for longer messages (according to which the size of EM' must correspond to the size of the modulus of the key).
Key Import
The posted key is a PEM encoded private key in PKCS#1 format.
.NET supports the import of PEM encoded keys (private/public, PKCS#8/PKCS#1 format) with ImportFromPem() since .NET 5, but the import of DER encoded keys has been supported since .NET Core 3.0. A private DER encoded key in PKCS#1 format can be imported with ImportRSAPrivateKey() (the conversion between PEM and DER encoding is trivial and consists of removing header, footer and line breaks and Base64 decoding of the remaining body).
BouncyCastle supports the import of a PEM encoded key with the PemReader class.
A possible implementation of the posted OpenSSL functionality with BouncyCastle
The following code generates the same signature as the OpenSSL statement when rasi.bin holds the data from dataToSign and riktest.key holds the key from privatePkcs1Pem:
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;
...
// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = #"-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
-----END RSA PRIVATE KEY-----";
byte[] dataToSign = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");
// Import private PKCS#1 key, PEM encoded
PemReader pemReader = new PemReader(new StringReader(privatePkcs1Pem));
AsymmetricKeyParameter privateKeyParameter = ((AsymmetricCipherKeyPair)pemReader.ReadObject()).Private;
// Sign raw data
ISigner signer = SignerUtilities.GetSigner("NoneWithRSA");
signer.Init(true, privateKeyParameter);
signer.BlockUpdate(dataToSign, 0, dataToSign.Length);
byte[] signature = signer.GenerateSignature();
Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C
Special use case - when the native C# methods can be used
If rasi.bin contains the DER encoding of the DigestInfo value, BouncyCastle is not needed.
The following example assumes that rasi.bin contains the DER encoding of the DigestInfo value for the message The quick brown fox jumps over the lazy dog with SHA256 as digest. I.e. the last 32 bytes correspond to the SHA256 hash.
A possible implementation with native .NET methods is then:
using System;
using System.Security.Cryptography;
...
// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = #"-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
-----END RSA PRIVATE KEY-----";
byte[] sha256DigestInfoDer = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");
byte[] sha256HashToSign = new byte[32];
Buffer.BlockCopy(sha256DigestInfoDer, sha256DigestInfoDer.Length - sha256HashToSign.Length, sha256HashToSign, 0, sha256HashToSign.Length);
using (RSA rsa = RSA.Create())
{
rsa.ImportFromPem(privatePkcs1Pem);
byte[] signature = rsa.SignHash(sha256HashToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // pass the SHA256 hash, internally the DER encoding of the DigestInfo is generated (which is why the digest must be specified)
Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C
}
which gives the same signature, since rasi.bin is identical in both cases.
However, keep in mind that the last approach only works if rasi.bin contains the DER encoding of the DigestInfo value, while the first solution works for arbitrary data in rasi.bin (as long as the length criterion is met).

Load ECIES public key

[Generated public key following (https://jameshfisher.com/2017/04/14/openssl-ecc/)
Alice generates her private key:
openssl ecparam -name secp256k1 -genkey -noout -out alice_priv_key.pem
Alice extracts her public key from her private key:
openssl ec -in alice_priv_key.pem -pubout -out alice_pub_key.pem
I have a application in C when I need to send encrypted data using public key. I am trying to load the data from public (alice_pub_key.pem) file.
I converted PEM file to hex data from (https://holtstrom.com/michael/tools/hextopem.php).
So seems I am not providing data in proper format. Can anyone suggest whats going wrong?
key = EC_KEY_new_by_curve_name(NID_secp256k1)
group = EC_KEY_get0_group(key)
pub_key = EC_POINT_new(group)
EC_POINT * point = EC_POINT_hex2point(group, ptr, pub_key, NULL)
This point its returning null.
You can try PEM_read_EC_PUBKEY to read the public key. This avoids having to parse or re-encode the public key itself.
Note that you may need a recent OpenSSL for this; it is hard to find out from the documentation when this function was introduced.

RSA encrypt a string from PEM public key in dotnet core

I'm trying to encrypt a string using a PEM public key file in dotnet core (1.1) and my question is: How do I do this?
I thought this would simply be a case of:
var cert = new X509Certificate2("path_to_public_key.pem");
using (var rsa = cert.GetRSAPublicKey())
{
// encrypt here
}
However, when I try to new up the certificate I get the following error:
error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error
The PEM file looks like this:
-----BEGIN PUBLIC KEY-----
... ANBgkqhkiG9w0BAQEFA ...
(loads more text)
-----END PUBLIC KEY-----
I've checked the pem file and there are no extraneous ^M characters anywhere (as from a suggest cause of the error here).
(I'm on macOS Sierra if that makes any difference)
Any help would be greatly appreciated!
Artjom B's comment sent me down the right path. A public key is indeed not a certificate.
Solution came from this excellent post.
For dotnet core (which doesn't have a RSACryptoServiceProvider class) I made a couple of minor modifications. You can return an RSAParameters object from GetRSAProviderFromPemFile, you can then use that like:
using (var rsa = RSA.Create()
{
rsa.ImportParameters(GetRSAProviderFromPemFile("path_to_pem.pem"));
var encrypted = rsa.Encrypt( ... );
}

Getting BadPaddingException while doing a Sqoop import with encrypted password file

I encrypt file with openssl then put it on HDFS, I used AES/ECB, 128 bits and salt option, and with some research I find out openssl uses PKCS5 padding as default which are all defaults in CryptoFileLoader class. Here is my encryption process:
# echo -n "password" > .pw
# openssl enc -aes-128-ecb -salt -in .pw -out .pw.enc
# hdfs dfs -put .pw.enc /user/user1/
Sqoop version is 1.4.6
Command:
sqoop import \
-Dorg.apache.sqoop.credentials.loader.class=org.apache.sqoop.util.password.CryptoFileLoader \
-Dorg.apache.sqoop.credentials.loader.crypto.passphrase=sqoop \
--connect jdbc:oracle:thin:#host/database \
--username user1 \
--password-file /user/user1/.pw.enc \
--table db.table1 \
--hive-import \
--hive-overwrite \
--hive-table hivedb.table1 \
--hive-drop-import-delims
which gives:
17/03/08 15:10:37 WARN tool.BaseSqoopTool: Failed to load password file
java.io.IOException: Can't decrypt the password
at org.apache.sqoop.util.password.CryptoFileLoader.loadPassword(CryptoFileLoader.java:151)
at org.apache.sqoop.util.CredentialsUtil.fetchPasswordFromLoader(CredentialsUtil.java:81)
at org.apache.sqoop.util.CredentialsUtil.fetchPassword(CredentialsUtil.java:66)
at org.apache.sqoop.tool.BaseSqoopTool.applyCredentialsOptions(BaseSqoopTool.java:1042)
at org.apache.sqoop.tool.BaseSqoopTool.applyCommonOptions(BaseSqoopTool.java:997)
at org.apache.sqoop.tool.ImportTool.applyOptions(ImportTool.java:875)
at org.apache.sqoop.tool.SqoopTool.parseArguments(SqoopTool.java:435)
at org.apache.sqoop.Sqoop.run(Sqoop.java:131)
at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70)
at org.apache.sqoop.Sqoop.runSqoop(Sqoop.java:179)
at org.apache.sqoop.Sqoop.runTool(Sqoop.java:218)
at org.apache.sqoop.Sqoop.runTool(Sqoop.java:227)
at org.apache.sqoop.Sqoop.main(Sqoop.java:236)
Caused by: javax.crypto.BadPaddingException: Given final block not properly padded
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at org.apache.sqoop.util.password.CryptoFileLoader.loadPassword(CryptoFileLoader.java:149)
... 12 more
Error while loading password file: Can't decrypt the password
I tried manually giving the other CryptoFileLoader parameters too and also passing local file to the --password-file .
I can decrypt the file back successfully with openssl. I can't decrypt with Java program(?)
I saw there is an issue with padding but I didn't know what it is and how to encrypt the file with a certain padding method or whatever else to do, I'm not experienced with encryption.
There is also org.apache.sqoop.credentials.loader.crypto.iterations parameter in the class which indicates number of PBKDF2 iterations but I don't know if it changes anything.
Thanks for any help.
I am not expert with Sqoop and Hadoop but starting from your exception
CryptoFileLoader.loadPassword(CryptoFileLoader.java:151)
I gave a look at the source code of CryptoFileLoader.java
It seems to me that things are a bit different from what you do: the password is stored in an encrypted file using the PBKDF2 algorithm, which is not equivalent to apply AES-128-ECB. From wikipedia:
PBKDF2 applies a pseudorandom function, such as hash-based message authentication code (HMAC), to the input password or passphrase along with a salt value and repeats the process many times to produce a derived key, which can then be used as a cryptographic key in subsequent operations. The added computational work makes password cracking much more difficult, and is known as key stretching.
There is no way to do PBKDF2 from Openssl command line. I made a small test using Java, it could be an alternative
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class Test {
/*Default is AES in electronic code book with padding.*/
private static String DEFAULT_ALG = "AES/ECB/PKCS5Padding";
/*Default salt is not much secure, use your own!*/
private static String DEFAULT_SALT = "SALT";
/*Iterate 10000 times by default.*/
private static int DEFAULT_ITERATIONS = 10000;
/*One of valid key sizes for default algorithm (AES).*/
private static int DEFAULT_KEY_LEN = 128;
public static void main(String[] args) throws IOException {
String inputFileName = "C:\\temp\\in.txt"; /*Enter your input (plain) file path */
String outputFileName = "C:\\temp\\out.bin"; /*Enter your output (encrypted) file path */
String passPhrase = "mypassphrase"; /*Enter your passphrase */
String salt = DEFAULT_SALT;
String alg = DEFAULT_ALG;
int iterations = DEFAULT_ITERATIONS;
int keyLen = DEFAULT_KEY_LEN;
SecretKeyFactory factory = null;
try {
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IOException("Can't load SecretKeyFactory", e);
}
SecretKeySpec key = null;
try {
String algOnly = alg.split("/")[0];
key = new SecretKeySpec(
factory.generateSecret(
new PBEKeySpec(passPhrase.toCharArray(), salt.getBytes(), iterations, keyLen)).getEncoded(),
algOnly);
} catch (Exception e) {
throw new IOException("Can't generate secret key", e);
}
Cipher crypto = null;
try {
crypto = Cipher.getInstance(alg);
} catch (Exception e) {
throw new IOException("Can't initialize the decryptor", e);
}
Path inputFileLocation = Paths.get(inputFileName);
byte[] decrypted = Files.readAllBytes(inputFileLocation);
byte[] encrypted;
try {
crypto.init(Cipher.ENCRYPT_MODE, key);
encrypted = crypto.doFinal(decrypted);
} catch (Exception e) {
throw new IOException("Can't decrypt the password", e);
}
Path outputFileLocation = Paths.get(outputFileName);
Files.write(outputFileLocation, encrypted);
}
}
As in Simone's answer - there is a difference in the encryption algorithm between openssl and java implementation. That is why you can decrypt using openssl with no problem (as it is invoking its own (different) algorithm again).
After much digging I found this answer from (Dave Thompson) which states:
Short answer: what openssl enc (without -K for raw) uses is not PBKDF2; it is almost PBKDF1, with iteration count 1.
It seems that there are two ways round this issue, either:
a) Find something in java that can decrypt what openssl is doing - there is a java library 'BouncyCastle' referenced in a post inside this answer (if you are happy using that instead of standard CryptoFile) where they have implemented the exact same algorithm that openssl is using.
or
b) Find some other command line utility to use instead of openssl that implements PBKDF2. A number of implementations in different languages are referenced in the nabble.com posting also mentioned.
(Due credit to Dave for the key observation quoted)

Encrypt string in JavaScript and decrypt in PHP with RSA technique

I am trying to encrypt some text in JavaScript and then send it to PHP (etc: with Ajax) to decrypt it there and save it (etc: In MySQL).
Here is my code so far:
In JavaScript:
I am using this library for the encryption:
http://travistidwell.com/blog/2013/02/15/a-better-library-for-javascript-asymmetrical-rsa-encryption/
function ConvertToURL(data) {
// Converts data to URL friendly form
// etc: Replaces '+', '/', '=' with 'plus', 'slash', 'equal'
};
function AjaxOrder(data) {
// Sends data in PHP with Ajax
}
var publicKey = '-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlOJu6TyygqxfWT7eLtGDwajtN
FOb9I5XRb6khyfD1Yt3YiCgQWMNW649887VGJiGr/L5i2osbl8C9+WJTeucF+S76
xFxdU6jE0NQ+Z+zEdhUTooNRaY5nZiu5PgDB0ED/ZKBUSLKL7eibMxZtMlUDHjm4
gwQco1KRMDSmXSMkDwIDAQAB
-----END PUBLIC KEY-----';
var encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
var encrypted = encrypt.encrypt('Text to send.');
*// And now I am sending the encrypted text with some Ajax function*
AjaxOrder(ConvertToURL(encrypted));
In PHP:
$dataPost = $_POST('dt');
function ConvertFromURL($data) {
// Converts $data to original form
// etc: Replaces 'plus', 'slash', 'equal' with '+', '/', '='
}
function ReturnData($data) {
// Sends $data back in JavaScript as an answer to Ajax
}
$privateKey = '-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDlOJu6TyygqxfWT7eLtGDwajtNFOb9I5XRb6khyfD1Yt3YiCgQ
WMNW649887VGJiGr/L5i2osbl8C9+WJTeucF+S76xFxdU6jE0NQ+Z+zEdhUTooNR
aY5nZiu5PgDB0ED/ZKBUSLKL7eibMxZtMlUDHjm4gwQco1KRMDSmXSMkDwIDAQAB
AoGAfY9LpnuWK5Bs50UVep5c93SJdUi82u7yMx4iHFMc/Z2hfenfYEzu+57fI4fv
xTQ//5DbzRR/XKb8ulNv6+CHyPF31xk7YOBfkGI8qjLoq06V+FyBfDSwL8KbLyeH
m7KUZnLNQbk8yGLzB3iYKkRHlmUanQGaNMIJziWOkN+N9dECQQD0ONYRNZeuM8zd
8XJTSdcIX4a3gy3GGCJxOzv16XHxD03GW6UNLmfPwenKu+cdrQeaqEixrCejXdAF
z/7+BSMpAkEA8EaSOeP5Xr3ZrbiKzi6TGMwHMvC7HdJxaBJbVRfApFrE0/mPwmP5
rN7QwjrMY+0+AbXcm8mRQyQ1+IGEembsdwJBAN6az8Rv7QnD/YBvi52POIlRSSIM
V7SwWvSK4WSMnGb1ZBbhgdg57DXaspcwHsFV7hByQ5BvMtIduHcT14ECfcECQATe
aTgjFnqE/lQ22Rk0eGaYO80cc643BXVGafNfd9fcvwBMnk0iGX0XRsOozVt5Azil
psLBYuApa66NcVHJpCECQQDTjI2AQhFc1yRnCU/YgDnSpJVm1nASoRUnU8Jfm3Oz
uku7JUXcVpt08DFSceCEX9unCuMcT72rAQlLpdZir876
-----END RSA PRIVATE KEY-----';
openssl_private_decrypt(ConvertFromURL($dataPost), $decryptedWord, $privateKey);
ReturnData(base64_encode($decryptedWord));
Now the answer from PHP is empty every time. Any ideas to make this work?
Thank you for your time!
Use HTTPS.
What you are doing will never be able to protect you against active attacks (MitM) since you don't have any trust anchors, and it is very likely that you will make some stupid mistake that will make it insecure.
Either way, you cannot encrypt more than a few hundred bytes directly with RSA. Thus, you will have to securely generate a random symmetric key (doing that properly in JavaScript is not easy), encrypt the data with it using a secure symmetric cipher (e.g. AES) in a secure block cipher mode, then encrypt the symmetric key with RSA. Learning how to do it "properly" will take you much more time than really doing it properly, and that is, configuring SSL.

Resources