13

我想更新 Google AMP 缓存,因此我需要按照此处所述对 URL 进行签名。

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

我一直在阅读这些帖子:

  1. 使用 /update-cache 请求更新 AMP 页面
  2. 如何通过 .NET 使用 RSA 和 SHA256 对文件进行签名?

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

我从我的 SSL 提供商那里得到了什么:、、、 www_example_com.crtwww_example_com.p7bcertificate code下文)。

我采取的步骤:

  1. test-public-key.cer通过打开www_example.com.crt并使用Copy to file向导将其复制到 base64 编码的 .cer 文件来创建。
  2. certificate code我将从 SSL 提供商处收到的信息保存为 file 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

按照此页面上的导出步骤,我能够生成一个 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.pem一个certificate-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?我正在查看此页面上的“生成 RSA 密钥”部分,但我很困惑,因为我实际上是否已经编写了这些步骤?如何检查我现在最终使用的 URL 是否有效?

更新 3

好的,我尝试了您的新代码。还是得到URL signature verification error。我尝试使用/amp我的文章的 URL 和没有/amp我的 URL 中的部分。两者都会导致相同的 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编辑URL 。但我想这会导致签名和实际 URL 之间的差异。难道是我的代码以某种方式破坏了字符,因此当我稍后手动重命名这些字符时,它总是会导致签名验证?你看到我可以在我的代码中修复什么了吗?amp_tsamp_url_signature&

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

请求的 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.pemapikey.pub放置它https://example.com/.well-known/amphtml/apikey.pub

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

我得到错误:unable to load certificates

但是这次我没有.crt文件,只有public-key.pem. 我怎样才能得到一个.pfx文件?我已经在这里检查过了。

4

2 回答 2

4

我将从 SSL 提供商处收到的证书代码保存为文件 test-private-key.cer

...

证书代码

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

以某种格式存储的文件

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

是一个基本上包含公钥的证书。它不包含私钥。这就是为什么当您X509Certificate2从此类文件创建实例时,它的HasPrivateKey属性设置为FalsePrivateKey返回Nothing,并且预期会抛出以下语句NullReferenceException

privateCert.PrivateKey.ToXmlString(True)

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

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

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

使用 SSL 工具创建 pfx 文件非常容易。如果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属性设置为TruePrivateKey返回 的实例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:“指定的算法无效。”

根本原因是RSACryptoServiceProvider 不支持 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)

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

错误 BC30456:“GetRSAPrivateKey”不是“X509Certificate2”的成员

更新 2(关于 URL 验证)

您引用的页面openssl包含用于验证签名的命令:

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

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

如何检查我现在最终使用的 URL 是否有效?

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

更新 3(关于签名验证失败)

更新 AMP 内容页面包含以下用于对 URL 进行签名的命令行:

echo -n >url.txt '/update-cache/c/s/example.com/article?amp_action=flush&_ts=1484941817' cat 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 实用程序和上面代码的两个签名都匹配了。如果修复后您仍然URL signature verification error从 Google AMP 获得,那么问题将出在传递用于签名的输入 URL 上。

更新 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

于 2018-04-05T07:54:23.840 回答
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。但是,之后可能需要进行一些修改,因为我不知道转换器的准确性。

于 2018-04-05T08:22:18.323 回答