【问题标题】:ASP.NET: Sign URL with public and private keys from filesASP.NET:使用文件中的公钥和私钥签署 URL
【发布时间】:2018-09-03 19:07:37
【问题描述】:

我想更新 Google AMP 缓存,因此我需要按照here 的说明对 URL 进行签名。

我的主要问题:我正在为如何获取证书/密钥以及如何将它们包含在下面的代码中而苦苦挣扎。 我只是找不到任何适用于 Windows 和 IIS 的全部说明。

我一直在阅读这些帖子:

  1. Using /update-cache requests to update AMP pages
  2. How can I sign a file using RSA and SHA256 with .NET?

我不想使用我的计算机的证书存储,如第二篇文章中所述。我宁愿将磁盘上的文件用于公钥和私钥。 从我的生产服务器 IIS 中,我将我的证书导出到一个 .pfx 文件,然后使用this site 底部的说明从中提取私钥和证书。 server.key 包含-----BEGIN RSA PRIVATE KEY-----,如果我使用它加载到下面代码中的privateCert 变量中,则会抛出错误Cannot find the requested object.

我从 SSL 提供商那里得到的信息: www_example_com.crtwww_example_com.p7bcertificate code(见下文)。

我已采取的步骤:

  1. 我通过打开www_example.com.crt 并使用Copy to file 向导将其复制到base64 编码的.cer 文件来创建test-public-key.cer
  2. 我保存了从 SSL 提供商处收到的 certificate code 文件 test-private-key.cer

当我运行以下代码时出现错误

对象引用未设置为对象的实例。

在线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 函数

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

-----开始证书-----
MIIF (...) DXuJ
-----结束证书-----

更新 1

按照this page 上的导出步骤,我能够生成一个 mysite.pfx 文件。 在向导中,我确保选择“是,导出私钥”并添加了密码。其余步骤我逐字执行。

然后我还运行了这些命令:
openssl pkcs12 -in mysite.pfx -nocerts -out private-key-VPS.pem
penssl pkcs12 -in mysite.pfx -clcerts -nokeys -out certificate-VPS.pem

我最终得到了 private-key-VPS.pemcertificate-VPS.pem 文件

我知道获取 mysite.pfx 的步骤与@CodeFuller 所描述的略有不同,但到目前为止一切顺利吗?

然后我添加了代码:

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)

但我得到 4 个错误:

GetRSAPrivateKey' 不是“X509Certificate2”的成员。
“SignData”不是“RSA”的成员。
'HashAlgorithmName' 未声明。由于其保护级别,它可能无法访问。
未声明“RSASignaturePadding”。由于其保护级别,它可能无法访问。

更新 2

感谢@CodeFuller!我的目标是框架 4.6.1,现在似乎更进一步。我最终得到一个这样的 URL: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==。我现在如何检查它是否是有效的 URL? 我正在检查this page 上的“生成 RSA 密钥”部分,但我很困惑,因为我实际上已经对这些步骤进行了编码吗?如何检查我现在最终使用的 URL 是否有效?

更新 3

好的,我尝试了您的新代码。仍然得到URL signature verification error。我尝试了我文章的/amp URL 和我的URL 中没有/amp 部分。两者都会导致相同的 URL 签名验证错误。

我注意到,当我将最终 URL 打印到我的网站(参见下面的代码)时,URL 显示为:

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==

注意参数应该是amp_tsamp_url_signature,现在它们分别是_ts_url_signature

在调用 Google 之前,我尝试通过手动将参数 _ts_url_signature 重命名为 amp_tsamp_url_signature 来编辑 URL。但我想这会导致签名和实际 URL 之间的差异。难道是我的代码以某种方式破坏了& 字符,因此当我稍后手动重命名这些字符时,它总是会导致签名验证?你看到我可以在我的代码中修复什么了吗?

顺便说一句:在签署 URL 之前,我尝试在代码隐藏中将 & 替换为 %26,但随后出现 Google 404 错误:

请求的 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== 在此服务器上未找到。这就是我们所知道的。

我的代码:

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>"

此外,我确定此页面存在于 Google AMP 缓存中,因为我可以在我的移动设备上的 Google 搜索结果中看到并请求它。

更新 4

我想我已经接近了,还可以得到一些额外的帮助,请参阅此处:https://github.com/ampproject/amphtml/issues/14483#issuecomment-380549060

我现在正在尝试让其他人也更容易测试:我现在运行以下命令来获取公钥和私钥,而不是依赖于我的 SSL

openssl genrsa 2048 > private-key.pem
openssl rsa -in private-key.pem -pubout >public-key.pem

我现在有文件 private-key.pempublic-key.pem

我会将public-key.pem 重命名为apikey.pub 并将其放在https://example.com/.well-known/amphtml/apikey.pub

我想采用@CodeFuller 推荐的最简单方法并创建一个.pfx 文件,然后我可以将其加载到X509Certificate2 类型的变量中。 当我运行这个命令时:
openssl pkcs12 -export -out keys.pfx -inkey private-key.pem -in public-key.pem

我收到错误:unable to load certificates

但是这次我没有.crt 文件,只有public-key.pem。如何获得.pfx 文件?我已经检查了here

【问题讨论】:

    标签: asp.net ssl certificate rsa google-amp


    【解决方案1】:

    我将我从 SSL 提供商收到的证书代码保存为文件 测试私钥.cer

    ...

    证书代码

    -----开始证书-----
    MIIF (...) DXuJ
    -----结束证书-----

    以某种格式存储的文件

    -----BEGIN CERTIFICATE----- 
    MIIF (...) DXuJ 
    -----END CERTIFICATE-----
    

    是一个基本上包含公钥的证书。它does not contain 私钥。这就是为什么当你从这样的文件创建X509Certificate2 的实例时,它的HasPrivateKey 属性设置为False 并且PrivateKey 返回Nothing,并且下面的语句预期会抛出NullReferenceException

    privateCert.PrivateKey.ToXmlString(True)
    

    为了签署数据,您需要一个私钥。私钥有以下格式

    -----BEGIN RSA PRIVATE KEY-----
    MIICXQ...B7Bou+
    -----END RSA PRIVATE KEY-----
    

    此类私钥通常存储在 *.key 或 *.pem(隐私增强邮件)文件中。 There is no 从 pem 文件加载 X509Certificate2 实例的内置方法。有很多可用的代码示例如何做到这一点,您可以在上面链接的问题中找到它们。然而,最简单的解决方案是创建 pfx 文件(包含私钥和公钥)。然后你可以很容易地用X509Certificate2的相应构造函数加载pfx。

    pfx 文件的创建是very easy with SSL tool。如果private.key 包含私钥(-----BEGIN RSA PRIVATE KEY-----),public.crt 包含公钥(-----BEGIN CERTIFICATE-----),您可以使用以下命令创建 pfx 文件:

    openssl pkcs12 -export -out keys.pfx -inkey private.key -in public.crt

    您将被要求输入密码。当您将密钥加载到X509Certificate2 时,也会使用此密码:

    Dim certificate As X509Certificate2 = New X509Certificate2("d:\CodeFuller\_days\2018.04.05\keys.pfx", "Password here")
    

    现在HasPrivateKey 属性设置为True 并且PrivateKey 返回RSACryptoServiceProvider 的实例。

    更新

    关于此代码:

    '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))
    

    RSACryptoServiceProvider 的实例实际上存储在certificate.PrivateKey 中,因此您可以避免使用上述代码并将其替换为:

    Dim provider As RSACryptoServiceProvider = certificate.PrivateKey
    

    但是,您当前的 SignData() 呼叫将不起作用:

    Dim sig() As Byte = key.SignData(data, CryptoConfig.MapNameToOID("SHA256"))
    

    这将引发以下异常:

    System.Security.Cryptography.CryptographicException: '无效 指定算法。'

    根本原因是RSACryptoServiceProviderdoes not support SHA256。这就是为什么我建议用RSACng 替换它的原因如下:

    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)
    

    方法 GetRSAPrivateKey 仅在 .NET Framework 4.6 之后才添加到 X509Certificate2 类中,因此如果出现以下错误,请考虑升级:

    错误 BC30456:“GetRSAPrivateKey”不是 'X509Certificate2'

    更新 2(关于 URL 验证)

    您引用的The page 包含用于验证签名的openssl 命令:

    openssl dgst -sha256 -signature signature.bin -verify public-key.pem url.txt

    但是,在您的情况下,这只是一个健全性检查,因为您刚刚使用有效程序生成了签名。所以回答你的问题:

    如何检查我现在得到的 URL 是否有效?

    最好的检查是使用签名 URL 向 AMP Cache 发送请求并检查响应。我以前没有使用过 AMP Cache,但我相信如果签名无效,它会响应一些 HTTP 错误。

    UPDATE 3(关于签名验证失败)

    Update AMP Content page 包含以下用于签署 URL 的命令行:

    echo -n >url.txt '/update-cache/c/s/example.com/article?amp_action=flush&amp_ts=1484941817' 猫 url.txt | openssl dgst -sha256 -sign private-key.pem >signature.bin

    我已将此命令构建的结果签名与我的答案中的代码计算的签名进行了比较。事实证明它们是不同的。我研究了可能的根本原因,发现问题是由我们获取 URL 字节的方式引起的。目前是:

    Dim data() As Byte = System.Text.Encoding.Unicode.GetBytes(signatureUrl)

    但是,我们应该对以 ASCII 表示的 URL 进行签名。所以将上面的行替换为:

    Dim data() As Byte = System.Text.Encoding.ASCII.GetBytes(signatureUrl)

    现在来自 openssl 实用程序和上面代码的两个签名都匹配了。如果修复后您仍然从 Google AMP 获得 URL signature verification error,那么问题将出在为签名传递的输入 URL 上。

    UPDATE 4(从私钥和公钥获取 PFX)

    生成私钥:

    openssl genrsa 2048 > 私钥.pem

    生成公钥:

    openssl rsa -in private-key.pem -pubout > public-key.pem

    创建证书签名请求:

    openssl req -new -key private-key.pem -out certificate.csr

    创建证书:

    openssl x509 -req -days 365 -in certificate.csr -signkey private-key.pem -out public.crt

    您将在此处被要求提供一些证书字段,例如国家名称、组织名称等。您使用哪个值并不重要,因为您需要此证书用于测试目的。

    创建 pfx 文件:

    openssl pkcs12 -export -out keys.pfx -inkey private-key.pem -in public.crt

    【讨论】:

    • 原来GetRSAPrivateKey被添加到X509Certificate2只在.NET Framework 4.6。是否可以将您的应用程序重新定位到 4.6?
    • 我还添加了关于RSACryptoServiceProvider 的答案的更新,请检查。您的更新中的其余部分似乎还可以,现在签名应该完成且没有错误。
    • @CodeFuller RSACryptoServiceProvider 有时支持 SHA-2。在您的情况下,如果您将 -csp "Microsoft Enhanced RSA and AES Cryptographic Provider" 添加到 pkcs12 命令中。 (OpenSSL 默认为无法执行 SHA-2 的旧 CSP)
    • @Flo,检查我关于 URL 验证的答案的更新。
    • @CodeFuller:URL 签名验证存在问题,我为此创建了一个单独的问题,因为目前尚不清楚它是否与您的代码示例或我调用 AMP 的方式有关缓存。仅供参考:在这里找到它stackoverflow.com/questions/49692957/…
    【解决方案2】:

    这是我用于使用磁盘证书对字符串进行签名的代码。我对其进行了修改以在 url 中使用。或许对你有帮助。

    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);
        }
    }
    

    用法:

    string originalString = "This is a test string";
    string signedString = signString(originalString);
    bool stringIsCorrect = verifyString(originalString, signedString);
    

    我现在看到你使用 VB。您可以使用它来转换代码http://www.carlosag.net/tools/codetranslator。但是,之后可能需要进行一些修改,因为我不知道转换器的准确性。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-04
      • 2020-03-06
      • 2015-12-28
      • 2022-08-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多