Our app generates a root CA + a server certificate when bootstrapping for internal use. We do not use the Keychain (the app is multiplatform), however are finding ourselves fairly limited by dependencies on it from what appears to be Apple Cryptography (.NET Core 2.x uses it internally).
We are using BouncyCastle as our crypto library.
It appears that every time we generate (or attempt to) generate a cert of any kind, it makes its way into the user's login keychain. This is not intentional, and causes problems in a purely daemon-ic environment where no UI sessions are running (so the keychain cannot be written to).
The app does not do this in Windows or Linux, so we're very curious where this is coming from. Ideally, we'd like to stop interacting with the keychain completely.
Our certificate class (in its entirety) is available here: https://paste.ee/p/CiXo3#9TFSTycJqh5E1xTNzt9vtBbT7ZOyB4zk
However, I'll quote the relevant functions being called here as well:
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;
}
On MacOS, various Keychain exceptions (one quoted below) are noted:
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
Remove the PersistKeySet
flag.
On Windows that flag says that the imported private key file should never be deleted, so you're slowly filling up the keys directory wherever that's running (and at some point you'll have so many of them that performance gets pretty bad).
On Linux that flag does nothing.
On macOS that flag causes the cert to be imported directly to the default keychain, because the only way to have a cert and key associated with each other is via SecIdentityRef, and only KeyChain can create those. Normally on macOS a PFX is loaded into a temporary keychain, but if it was loaded without exportable it now can't be moved, making Windows code of "load persisted and add it to an X509Store" not function.