在 OS X 上使用 X509Certificate2 时避免钥匙串
Avoiding the Keychain when using X509Certificate2 on OS X
我们的应用在引导供内部使用时会生成一个根 CA + 一个服务器证书。我们不使用钥匙串(该应用程序是多平台的),但是发现我们自己受到似乎是 Apple Cryptography(.NET Core 2.x 在内部使用它)对它的依赖的相当大的限制。
我们使用 BouncyCastle 作为我们的加密库。
似乎每次我们生成(或尝试生成)任何类型的证书时,它都会进入用户的登录钥匙串。这不是故意的,并且会在没有 UI 会话 运行 的纯守护进程环境中导致问题(因此无法写入钥匙串)。
该应用程序在 Windows 或 Linux 中不会执行此操作,所以我们很好奇这是从哪里来的。理想情况下,我们希望完全停止与钥匙串交互。
我们的证书 class(完整)可在此处获得:https://paste.ee/p/CiXo3#9TFSTycJqh5E1xTNzt9vtBbT7ZOyB4zk
但是,我也会引用这里调用的相关函数:
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages, string password = null)
{
// It's self-signed, so these are the same.
var issuerName = subjectName;
var random = GetSecureRandom();
var subjectKeyPair = GenerateKeyPair(random, 2048);
// It's self-signed, so these are the same.
var issuerKeyPair = subjectKeyPair;
var serialNumber = GenerateSerialNumber(random);
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
const bool isCertificateAuthority = true;
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
subjectAlternativeNames, issuerName, issuerKeyPair,
issuerSerialNumber, isCertificateAuthority,
usages);
return ConvertCertificate(certificate, subjectKeyPair, random, password);
}
public X509Certificate GenerateCertificate(SecureRandom random,
string subjectName,
AsymmetricCipherKeyPair subjectKeyPair,
BigInteger subjectSerialNumber,
string[] subjectAlternativeNames,
string issuerName,
AsymmetricCipherKeyPair issuerKeyPair,
BigInteger issuerSerialNumber,
bool isCertificateAuthority,
KeyPurposeID[] usages)
{
var certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.SetSerialNumber(subjectSerialNumber);
// Set the signature algorithm. This is used to generate the thumbprint which is then signed
// with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong.
const string signatureAlgorithm = "SHA256WithRSA";
certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
var issuerDN = new X509Name(issuerName);
certificateGenerator.SetIssuerDN(issuerDN);
// Note: The subject can be omitted if you specify a subject alternative name (SAN).
var subjectDN = new X509Name(subjectName);
certificateGenerator.SetSubjectDN(subjectDN);
// Our certificate needs valid from/to values.
var notBefore = DateTime.UtcNow.Date;
var notAfter = notBefore.AddYears(10);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// The subject's public key goes in the certificate.
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
AddBasicConstraints(certificateGenerator, isCertificateAuthority);
if (usages != null && usages.Any())
AddExtendedKeyUsage(certificateGenerator, usages);
if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
// The certificate is signed with the issuer's private key.
var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random);
return certificate;
}
public X509Certificate2 ConvertCertificate(X509Certificate certificate,
AsymmetricCipherKeyPair subjectKeyPair,
SecureRandom random, string password)
{
// Now to convert the Bouncy Castle certificate to a .NET certificate.
// See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
// ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that.
var store = new Pkcs12Store();
// What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
string friendlyName = certificate.SubjectDN.ToString();
// Add the certificate.
var certificateEntry = new X509CertificateEntry(certificate);
store.SetCertificateEntry(friendlyName, certificateEntry);
// Add the private key.
store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
// Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
// It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
var stream = new MemoryStream();
store.Save(stream, password.ToCharArray(), random);
var convertedCertificate =
new X509Certificate2(stream.ToArray(),
password,
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
return convertedCertificate;
}
在 MacOS 上,记录了各种钥匙串异常(下面引用了一个):
Unhandled Exception: Interop+AppleCrypto+AppleCommonCryptoCryptographicException: User interaction is not allowed.
at Interop.AppleCrypto.X509ImportCertificate(Byte[] bytes, X509ContentType contentType, SafePasswordHandle importPassword, SafeKeychainHandle keychain, Boolean exportable, SafeSecIdentityHandle& identityHandle)
at Internal.Cryptography.Pal.CertificatePal.FromBlob(Byte[] rawData, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(Byte[] rawData, String password, X509KeyStorageFlags keyStorageFlags)
at Spectero.daemon.Libraries.Core.Crypto.CryptoService.ConvertCertificate(X509Certificate certificate, AsymmetricCipherKeyPair subjectKeyPair, SecureRandom random, String password) in /opt/spectero/daemon/deploy/daemon/Libraries/Core/Crypto/CryptoService.cs:line 398
at Spectero.daemon.Migrations.Initialize.Up() in /opt/spectero/daemon/deploy/daemon/Migrations/Initialize.cs:line 116
at Spectero.daemon.Startup.Configure(IOptionsSnapshot`1 configMonitor, IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IMigration migration, IAutoStarter autoStarter, IServiceProvider serviceProvider) in /opt/spectero/daemon/deploy/daemon/Startup.cs:line 193
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
at Spectero.daemon.Program.Main(String[] args) in /opt/spectero/daemon/deploy/daemon/Program.cs:line 12
删除 PersistKeySet
标志。
在 Windows 上,该标志表明永远不应删除导入的私钥文件,因此您会慢慢填充 运行 所在的密钥目录(并且在某些时候您会它们太多了,以至于性能变得很差。
在 Linux 上,该标志不执行任何操作。
在 macOS 上,该标志导致证书直接导入到默认钥匙串,因为将证书和密钥相互关联的唯一方法是通过 SecIdentityRef,并且只有 KeyChain 可以创建它们。通常在 macOS 上,PFX 被加载到临时钥匙串中,但如果它在没有导出的情况下加载它现在无法移动,使得 Windows "load persisted and add it to an X509Store" 代码无法运行。
我们的应用在引导供内部使用时会生成一个根 CA + 一个服务器证书。我们不使用钥匙串(该应用程序是多平台的),但是发现我们自己受到似乎是 Apple Cryptography(.NET Core 2.x 在内部使用它)对它的依赖的相当大的限制。
我们使用 BouncyCastle 作为我们的加密库。
似乎每次我们生成(或尝试生成)任何类型的证书时,它都会进入用户的登录钥匙串。这不是故意的,并且会在没有 UI 会话 运行 的纯守护进程环境中导致问题(因此无法写入钥匙串)。
该应用程序在 Windows 或 Linux 中不会执行此操作,所以我们很好奇这是从哪里来的。理想情况下,我们希望完全停止与钥匙串交互。
我们的证书 class(完整)可在此处获得:https://paste.ee/p/CiXo3#9TFSTycJqh5E1xTNzt9vtBbT7ZOyB4zk
但是,我也会引用这里调用的相关函数:
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages, string password = null)
{
// It's self-signed, so these are the same.
var issuerName = subjectName;
var random = GetSecureRandom();
var subjectKeyPair = GenerateKeyPair(random, 2048);
// It's self-signed, so these are the same.
var issuerKeyPair = subjectKeyPair;
var serialNumber = GenerateSerialNumber(random);
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
const bool isCertificateAuthority = true;
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
subjectAlternativeNames, issuerName, issuerKeyPair,
issuerSerialNumber, isCertificateAuthority,
usages);
return ConvertCertificate(certificate, subjectKeyPair, random, password);
}
public X509Certificate GenerateCertificate(SecureRandom random,
string subjectName,
AsymmetricCipherKeyPair subjectKeyPair,
BigInteger subjectSerialNumber,
string[] subjectAlternativeNames,
string issuerName,
AsymmetricCipherKeyPair issuerKeyPair,
BigInteger issuerSerialNumber,
bool isCertificateAuthority,
KeyPurposeID[] usages)
{
var certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.SetSerialNumber(subjectSerialNumber);
// Set the signature algorithm. This is used to generate the thumbprint which is then signed
// with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong.
const string signatureAlgorithm = "SHA256WithRSA";
certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
var issuerDN = new X509Name(issuerName);
certificateGenerator.SetIssuerDN(issuerDN);
// Note: The subject can be omitted if you specify a subject alternative name (SAN).
var subjectDN = new X509Name(subjectName);
certificateGenerator.SetSubjectDN(subjectDN);
// Our certificate needs valid from/to values.
var notBefore = DateTime.UtcNow.Date;
var notAfter = notBefore.AddYears(10);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// The subject's public key goes in the certificate.
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
AddBasicConstraints(certificateGenerator, isCertificateAuthority);
if (usages != null && usages.Any())
AddExtendedKeyUsage(certificateGenerator, usages);
if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
// The certificate is signed with the issuer's private key.
var certificate = certificateGenerator.Generate(issuerKeyPair.Private, random);
return certificate;
}
public X509Certificate2 ConvertCertificate(X509Certificate certificate,
AsymmetricCipherKeyPair subjectKeyPair,
SecureRandom random, string password)
{
// Now to convert the Bouncy Castle certificate to a .NET certificate.
// See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
// ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that.
var store = new Pkcs12Store();
// What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
string friendlyName = certificate.SubjectDN.ToString();
// Add the certificate.
var certificateEntry = new X509CertificateEntry(certificate);
store.SetCertificateEntry(friendlyName, certificateEntry);
// Add the private key.
store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
// Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
// It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
var stream = new MemoryStream();
store.Save(stream, password.ToCharArray(), random);
var convertedCertificate =
new X509Certificate2(stream.ToArray(),
password,
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
return convertedCertificate;
}
在 MacOS 上,记录了各种钥匙串异常(下面引用了一个):
Unhandled Exception: Interop+AppleCrypto+AppleCommonCryptoCryptographicException: User interaction is not allowed.
at Interop.AppleCrypto.X509ImportCertificate(Byte[] bytes, X509ContentType contentType, SafePasswordHandle importPassword, SafeKeychainHandle keychain, Boolean exportable, SafeSecIdentityHandle& identityHandle)
at Internal.Cryptography.Pal.CertificatePal.FromBlob(Byte[] rawData, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(Byte[] rawData, String password, X509KeyStorageFlags keyStorageFlags)
at Spectero.daemon.Libraries.Core.Crypto.CryptoService.ConvertCertificate(X509Certificate certificate, AsymmetricCipherKeyPair subjectKeyPair, SecureRandom random, String password) in /opt/spectero/daemon/deploy/daemon/Libraries/Core/Crypto/CryptoService.cs:line 398
at Spectero.daemon.Migrations.Initialize.Up() in /opt/spectero/daemon/deploy/daemon/Migrations/Initialize.cs:line 116
at Spectero.daemon.Startup.Configure(IOptionsSnapshot`1 configMonitor, IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IMigration migration, IAutoStarter autoStarter, IServiceProvider serviceProvider) in /opt/spectero/daemon/deploy/daemon/Startup.cs:line 193
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
at Spectero.daemon.Program.Main(String[] args) in /opt/spectero/daemon/deploy/daemon/Program.cs:line 12
删除 PersistKeySet
标志。
在 Windows 上,该标志表明永远不应删除导入的私钥文件,因此您会慢慢填充 运行 所在的密钥目录(并且在某些时候您会它们太多了,以至于性能变得很差。
在 Linux 上,该标志不执行任何操作。
在 macOS 上,该标志导致证书直接导入到默认钥匙串,因为将证书和密钥相互关联的唯一方法是通过 SecIdentityRef,并且只有 KeyChain 可以创建它们。通常在 macOS 上,PFX 被加载到临时钥匙串中,但如果它在没有导出的情况下加载它现在无法移动,使得 Windows "load persisted and add it to an X509Store" 代码无法运行。