在 C# 中使用 ECDSA 生成证书
generate certificate using ECDSA in c#
我正在尝试使用 ECDSA 生成带有私钥的(自签名)证书。
目标是像使用 openssl:
一样获得 "the same" (pkcs12) 证书
openssl ecparam -genkey -name secp256r1 -out mykey.key
openssl req -new -key mykey.key -out myreq.csr
openssl req -x509 -days 7 -key mykey.key -in myreq.csr -out mycert.crt
openssl pkcs12 -export -out mycert.pfx -inkey mykey.key -in mycert.crt
我已经使用 BouncyCastle 来帮助我创建基于 RSA 的证书,所以接下来的步骤或多或少地遵循我用来创建 RSA 证书的方式。
(请注意,BC
前缀用于来自 BouncyCastle 的 classes,MS
用于 .NET classes)
1 生成密钥对:私钥和 public 密钥
BC.IAsymmetricCipherKeyPairGenerator bcKpGen = BC.GeneratorUtilities.GetKeyPairGenerator("ECDSA");
bcKpGen.Init(new BC.ECKeyGenerationParameters(BC.SecObjectIdentifiers.SecP256r1, new BC.SecureRandom()));
BC.AsymmetricCipherKeyPair bcSubjKeys = bcKpGen.GenerateKeyPair();
2 使用私钥签署 public 带有一些附加数据(主题、有效期等)的密钥
BC.X509V3CertificateGenerator bcXgen = new BC.X509V3CertificateGenerator();
// .. set subject, validity period etc
bcXgen.SetPublicKey(bcSubjKeys.Public);
BC.ISignatureFactory bcSigFac = new BC.Asn1SignatureFactory("SHA256WITHECDSA", bcSubjKeys.Private);
BC.X509Certificate bcCert = bcXgen.Generate(bcSigFac);
3 "join" 来自步骤 1 的私钥和来自步骤 2 的证书以获取带有私钥的证书。
如果我可以接受没有私钥的证书,我可以这样做:
MS.X509Certificate mcCert = new MS.X509Certificate2(bcCert.GetEncoded(), null);
我完成了。
尝试设置私钥时出现问题:
msCert.PrivateKey = ConvertBouncyToNetSomehow(bcSubjKeys.Private)
(注意msCert.PrivateKey
的类型是MS.AsymmetricAlgorithm
,bcSubjKeys.Private
的类型是BC.ECPrivateKeyParameters
)
似乎合适的方法是使用 MS.ECDsaCng
class(继承自 MS.AsymmetricAlgorithm
),但是:
1 我发现将 BC.ECPrivateKeyParameters
转换为 MS.CngKey
(MS.ECDsaCng
需要)的唯一方法是通过 pkcs8 格式:
BC.PrivateKeyInfo bcPKInfo = BC.PrivateKeyInfoFactory.CreatePrivateKeyInfo(bcSubjKeys.Private);
byte[] pkArr = bcPKInfo.GetDerEncoded();
MS.CngKey msPKCng = MS.CngKey.Import(pkArr, MS.CngKeyBlobFormat.Pkcs8PrivateBlob);
但是使用这种方法会丢失一些信息,因为 msPKCng.AlgorithmGroup
的值是 "ECDH"
而 bcSubjKeys.Private.AlgorithmName
表示 "ECDSA"
。此外,ECDH 密钥不能与 MS.ECDsaCng
.
一起使用
然而..我可以继续 MS.ECDiffieHellmanCng
而不是请求 MS.ECDsaCng
if..
2 MS.X509Certificate2.set_PrivateKey
的实现要求对象实现接口 MS.ICspAsymmetricAlgorithm
。但是他们(ECDsaCng
、ECDiffieHellmanCng
)都没有实现它。
在这一点上似乎必须使用不同的方法(因为 MS.ICspAsymmetricAlgorithm
条件),例如将证书和私钥导出到 pkcs 文件并使用 X509Certificate2.Import(..)
.
有什么提示吗?
此致
遗憾的是,现在无法直接开箱即用。您可以使用 P/Invokes 和 .NET 4.6.2(目前处于预览阶段)完成剩下的工作。或者,绕过 .NET Core,您可以构建一个在 .NET 4.6.1 中运行的 PFX。
“ECDSA”与“ECDH”
Windows CNG 库将 ECC 拆分为 ECDSA 和 ECDH。 ECDSA密钥对象只能用于ECDSA;但每当 Windows 无法确定 PFX 导入(或 PKCS#8 导入)期间的用法时,它会调用私钥 ECDH。为什么?因为Windows让ECDH密钥对象同时做密钥协商(ECDH)和数字签名(ECDSA),所以ECDH更加灵活。
但是 .NET 4.6.1 不知道这一点。
.NET Core 没有这个限制(见https://github.com/dotnet/corefx/pull/5850), and .NET 4.6.2 has also removed the restriction (per https://github.com/Microsoft/dotnet/blob/master/releases/net462/dotnet462-changes.md#user-content-bcl)。
正在生成“ECDSA”密钥,而不是“ECDH”
.NET Core 现在在 ECDsa 上有一个 ImportParameters 方法。如果可以将 BC.ECPrivateKeyProperty 对象转换为 MS.ECParameters 结构,则可以将 blob 导入 ECDsaCng 对象。 (务必将其用作命名曲线,而不是显式复制所有曲线参数)。
由于它是有意导入到 ECDsa 对象中的,因此它获得了 ECDSA 密钥,并且该信息将嵌入到 PFX 中。
构建 PFX(将它们捆绑在一起)
只需一点 P/Invoking,您就可以说服 Windows 使用临时密钥构建 PFX。虽然 .NET 无法从证书访问临时私钥,但如果从 PFX 加载,它将能够使用它:
[DllImport(Libraries.Crypt32, CharSet = CharSet.Unicode, SetLastError = true)]
private static extern unsafe bool CertSetCertificateContextProperty(IntPtr pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, SafeNCryptKeyHandle pvData);
internal enum CertContextPropId : int
{
CERT_NCRYPT_KEY_HANDLE_PROP_ID = 78,
}
[Flags]
internal enum CertSetPropertyFlags : int
{
None = 0,
}
private static X509Certificate2 MateECDsaPrivateKey(
X509Certificate2 cert,
CngKey privateKey)
{
// Make a new certificate instance which isn't tied to the current one
using (var tmpCert = new X509Certificate2(cert.RawData))
{
SafeNCryptKeyHandle keyHandle = privateKey.Handle;
// Set the ephemeral key handle property
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// You could emit this, if you prefer.
byte[] pfxBytes = tmpCert.Export(X509ContentType.Pkcs12);
// Clear the key handle out again to prevent double-free
keyHandle = new SafeNCryptKeyHandle();
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// Now load a new certificate which has a temporary keyfile on disk.
// Note: If you don't want exportability here, don't request it.
var matedCert = new X509Certificate2(pfxBytes, (string)null, X509KeyStorageFlags.Exportable);
using (ECDsa ecdsa = matedCert.GetECDsaPrivateKey())
{
if (ecdsa == null)
{
throw new InvalidOperationException("It didn't work");
}
}
return matedCert;
}
}
您需要 .NET 4.6.1(或更新版本)才能访问 GetECDsaPrivateKey()。
我能够通过使用 CertificateRequest 创建自签名 X509 并传递 ECDsa 密钥来使用 MateECDsaPrivateKey。然后使用 Export(X509ContentType.Cert) 导出 public 密钥。然后使用MateECDsaPrivateKey重新连接原来的Private key。
我的用例是创建一个 X509 证书来存储 ECDiffieHellman 所需的密钥。如果密钥不属于 ECDH 算法组,则底层 Cng 密钥处理不喜欢该密钥。即使您将底层 Cng 密钥类型算法组设置为 ECDH,它也会重置为 ECDsa。 ECDiffieHellmanCng 需要 ECDH
通过使用 MateECDsaPrivateKey,我能够将 X509 public 密钥重新连接到正确的 ECDH 私钥。这只是 Windows 上的一个问题,在非 Windows 平台上,底层密钥处理不那么挑剔。
我创建了 https://dev.azure.com/ctx-eng-automation-devops/ElipticCurveX509Certificates,它根据 OS 平台运行正确的进程。它还对 X509Certificate2 进行了扩展,以使用正确的方法提取 ECDH 私钥以创建 ECDiffieHellman 对象。
https://www.nuget.org/packages/ElipticCurveX509Certificates/
我正在尝试使用 ECDSA 生成带有私钥的(自签名)证书。 目标是像使用 openssl:
一样获得 "the same" (pkcs12) 证书openssl ecparam -genkey -name secp256r1 -out mykey.key
openssl req -new -key mykey.key -out myreq.csr
openssl req -x509 -days 7 -key mykey.key -in myreq.csr -out mycert.crt
openssl pkcs12 -export -out mycert.pfx -inkey mykey.key -in mycert.crt
我已经使用 BouncyCastle 来帮助我创建基于 RSA 的证书,所以接下来的步骤或多或少地遵循我用来创建 RSA 证书的方式。
(请注意,BC
前缀用于来自 BouncyCastle 的 classes,MS
用于 .NET classes)
1 生成密钥对:私钥和 public 密钥
BC.IAsymmetricCipherKeyPairGenerator bcKpGen = BC.GeneratorUtilities.GetKeyPairGenerator("ECDSA");
bcKpGen.Init(new BC.ECKeyGenerationParameters(BC.SecObjectIdentifiers.SecP256r1, new BC.SecureRandom()));
BC.AsymmetricCipherKeyPair bcSubjKeys = bcKpGen.GenerateKeyPair();
2 使用私钥签署 public 带有一些附加数据(主题、有效期等)的密钥
BC.X509V3CertificateGenerator bcXgen = new BC.X509V3CertificateGenerator();
// .. set subject, validity period etc
bcXgen.SetPublicKey(bcSubjKeys.Public);
BC.ISignatureFactory bcSigFac = new BC.Asn1SignatureFactory("SHA256WITHECDSA", bcSubjKeys.Private);
BC.X509Certificate bcCert = bcXgen.Generate(bcSigFac);
3 "join" 来自步骤 1 的私钥和来自步骤 2 的证书以获取带有私钥的证书。
如果我可以接受没有私钥的证书,我可以这样做:
MS.X509Certificate mcCert = new MS.X509Certificate2(bcCert.GetEncoded(), null);
我完成了。
尝试设置私钥时出现问题:
msCert.PrivateKey = ConvertBouncyToNetSomehow(bcSubjKeys.Private)
(注意msCert.PrivateKey
的类型是MS.AsymmetricAlgorithm
,bcSubjKeys.Private
的类型是BC.ECPrivateKeyParameters
)
似乎合适的方法是使用 MS.ECDsaCng
class(继承自 MS.AsymmetricAlgorithm
),但是:
1 我发现将 BC.ECPrivateKeyParameters
转换为 MS.CngKey
(MS.ECDsaCng
需要)的唯一方法是通过 pkcs8 格式:
BC.PrivateKeyInfo bcPKInfo = BC.PrivateKeyInfoFactory.CreatePrivateKeyInfo(bcSubjKeys.Private);
byte[] pkArr = bcPKInfo.GetDerEncoded();
MS.CngKey msPKCng = MS.CngKey.Import(pkArr, MS.CngKeyBlobFormat.Pkcs8PrivateBlob);
但是使用这种方法会丢失一些信息,因为 msPKCng.AlgorithmGroup
的值是 "ECDH"
而 bcSubjKeys.Private.AlgorithmName
表示 "ECDSA"
。此外,ECDH 密钥不能与 MS.ECDsaCng
.
然而..我可以继续 MS.ECDiffieHellmanCng
而不是请求 MS.ECDsaCng
if..
2 MS.X509Certificate2.set_PrivateKey
的实现要求对象实现接口 MS.ICspAsymmetricAlgorithm
。但是他们(ECDsaCng
、ECDiffieHellmanCng
)都没有实现它。
在这一点上似乎必须使用不同的方法(因为 MS.ICspAsymmetricAlgorithm
条件),例如将证书和私钥导出到 pkcs 文件并使用 X509Certificate2.Import(..)
.
有什么提示吗? 此致
遗憾的是,现在无法直接开箱即用。您可以使用 P/Invokes 和 .NET 4.6.2(目前处于预览阶段)完成剩下的工作。或者,绕过 .NET Core,您可以构建一个在 .NET 4.6.1 中运行的 PFX。
“ECDSA”与“ECDH”
Windows CNG 库将 ECC 拆分为 ECDSA 和 ECDH。 ECDSA密钥对象只能用于ECDSA;但每当 Windows 无法确定 PFX 导入(或 PKCS#8 导入)期间的用法时,它会调用私钥 ECDH。为什么?因为Windows让ECDH密钥对象同时做密钥协商(ECDH)和数字签名(ECDSA),所以ECDH更加灵活。
但是 .NET 4.6.1 不知道这一点。
.NET Core 没有这个限制(见https://github.com/dotnet/corefx/pull/5850), and .NET 4.6.2 has also removed the restriction (per https://github.com/Microsoft/dotnet/blob/master/releases/net462/dotnet462-changes.md#user-content-bcl)。
正在生成“ECDSA”密钥,而不是“ECDH”
.NET Core 现在在 ECDsa 上有一个 ImportParameters 方法。如果可以将 BC.ECPrivateKeyProperty 对象转换为 MS.ECParameters 结构,则可以将 blob 导入 ECDsaCng 对象。 (务必将其用作命名曲线,而不是显式复制所有曲线参数)。
由于它是有意导入到 ECDsa 对象中的,因此它获得了 ECDSA 密钥,并且该信息将嵌入到 PFX 中。
构建 PFX(将它们捆绑在一起)
只需一点 P/Invoking,您就可以说服 Windows 使用临时密钥构建 PFX。虽然 .NET 无法从证书访问临时私钥,但如果从 PFX 加载,它将能够使用它:
[DllImport(Libraries.Crypt32, CharSet = CharSet.Unicode, SetLastError = true)]
private static extern unsafe bool CertSetCertificateContextProperty(IntPtr pCertContext, CertContextPropId dwPropId, CertSetPropertyFlags dwFlags, SafeNCryptKeyHandle pvData);
internal enum CertContextPropId : int
{
CERT_NCRYPT_KEY_HANDLE_PROP_ID = 78,
}
[Flags]
internal enum CertSetPropertyFlags : int
{
None = 0,
}
private static X509Certificate2 MateECDsaPrivateKey(
X509Certificate2 cert,
CngKey privateKey)
{
// Make a new certificate instance which isn't tied to the current one
using (var tmpCert = new X509Certificate2(cert.RawData))
{
SafeNCryptKeyHandle keyHandle = privateKey.Handle;
// Set the ephemeral key handle property
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// You could emit this, if you prefer.
byte[] pfxBytes = tmpCert.Export(X509ContentType.Pkcs12);
// Clear the key handle out again to prevent double-free
keyHandle = new SafeNCryptKeyHandle();
if (!CertSetCertificateContextProperty(
tmpCert.Handle,
CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
CertSetPropertyFlags.None,
keyHandle))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
// Now load a new certificate which has a temporary keyfile on disk.
// Note: If you don't want exportability here, don't request it.
var matedCert = new X509Certificate2(pfxBytes, (string)null, X509KeyStorageFlags.Exportable);
using (ECDsa ecdsa = matedCert.GetECDsaPrivateKey())
{
if (ecdsa == null)
{
throw new InvalidOperationException("It didn't work");
}
}
return matedCert;
}
}
您需要 .NET 4.6.1(或更新版本)才能访问 GetECDsaPrivateKey()。
我能够通过使用 CertificateRequest 创建自签名 X509 并传递 ECDsa 密钥来使用 MateECDsaPrivateKey。然后使用 Export(X509ContentType.Cert) 导出 public 密钥。然后使用MateECDsaPrivateKey重新连接原来的Private key。
我的用例是创建一个 X509 证书来存储 ECDiffieHellman 所需的密钥。如果密钥不属于 ECDH 算法组,则底层 Cng 密钥处理不喜欢该密钥。即使您将底层 Cng 密钥类型算法组设置为 ECDH,它也会重置为 ECDsa。 ECDiffieHellmanCng 需要 ECDH
通过使用 MateECDsaPrivateKey,我能够将 X509 public 密钥重新连接到正确的 ECDH 私钥。这只是 Windows 上的一个问题,在非 Windows 平台上,底层密钥处理不那么挑剔。
我创建了 https://dev.azure.com/ctx-eng-automation-devops/ElipticCurveX509Certificates,它根据 OS 平台运行正确的进程。它还对 X509Certificate2 进行了扩展,以使用正确的方法提取 ECDH 私钥以创建 ECDiffieHellman 对象。
https://www.nuget.org/packages/ElipticCurveX509Certificates/