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

丶灬走出姿态 提交于 2019-12-04 23:46:26

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!