I am trying to create a TLS1.1/TLS1.2 server using .Net's sslStream class. It appears that by default the only cipher suites that this stream accepts are:
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
I'd like to enable non-ECDHE versions of these (i.e. TLS_RSA_WITH_AES_128_CBC_SHA256). I googled around a bit and people talk about changing cipher suites by modifying the default SChannel settings -- either through the "SSL Cipher Suite Order" or using CNG functions: http://msdn.microsoft.com/en-us/library/windows/desktop/bb870930(v=vs.85).aspx
However, I tried this and I can't get anything to work. Using the above link's C++ code to list the enabled cipher suites with BCryptEnumContextFunctions() shows that the cipher suites I want are enabled by default. I even added TLS_RSA_WITH_AES_128_CBC_SHA256 as a top priority suite, and sslStream still refuses a TLS connection from a client that only supports that cipher (Exception: "The client and server cannot communicate, because they do not possess a common algorithm") Any idea what is going on here?
(By the way, if my client supports one of the ECDHE cipher suites, everything works great)
How are other folks implementing TLS in .Net 4.5? Should I be looking at opensource solutions? What about a wrapper for SChannel to use the CNG api more directly?
I contacted Microsoft's technical support and after using their proprietary tracing ability, it turned out that the certificate I had installed on my server did not have it's private key marked as an "exchange key". Apparently the private key counterpart of every public key in the certificate store has certain uses for which it it is allowed. In my case, the private key was only allowed to be used for signatures and was not allowed to be used for encrypting a symmetric key during the SSL/TLS handshake. This meant that my server could only support ECDHE cipher suites.
It also turns out that you can't check the enabled uses of a private key in the Certificate MMC snap-in. Making matters worse, using the sslStream class, there is also no way of determining any information for a handshake failure beyond the generic exception "The client and server cannot communicate, because they do not possess a common algorithm".
The final thing to mention is how I managed to install a server certificate with a restricted private key in the first place. It turns out that I generated it that way. I was using the CertEnroll COM interface to programmatically generate a certificate signing request which I exported, had a certificate authority sign, and installed the certificate authority's response. The C# code that I used to generate the certificate signing request accidentally created a private key that was only enabled for signature use.
From my experience, the CertEnroll interface is difficult to use and it's hard to find working examples online. So for others' reference I am including my C# code that generates a base64 encoded certificate signing request functional for SSL/TLS handshakes. In my case, the line objPrivateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; was missing.
using CERTENROLLLib;
using CERTCLILib;
public string GenerateRequest(string Subject, StoreLocation Location)
{
//code originally came from: http://blogs.msdn.com/b/alejacma/archive/2008/09/05/how-to-create-a-certificate-request-with-certenroll-and-net-c.aspx
//modified version of it is here: http://stackoverflow.com/questions/16755634/issue-generating-a-csr-in-windows-vista-cx509certificaterequestpkcs10
//here is the standard for certificates: http://www.ietf.org/rfc/rfc3280.txt
//the PKCS#10 certificate request (http://msdn.microsoft.com/en-us/library/windows/desktop/aa377505.aspx)
CX509CertificateRequestPkcs10 objPkcs10 = new CX509CertificateRequestPkcs10();
//assymetric private key that can be used for encryption (http://msdn.microsoft.com/en-us/library/windows/desktop/aa378921.aspx)
CX509PrivateKey objPrivateKey = new CX509PrivateKey();
//access to the general information about a cryptographic provider (http://msdn.microsoft.com/en-us/library/windows/desktop/aa375967.aspx)
CCspInformation objCSP = new CCspInformation();
//collection on cryptographic providers available: http://msdn.microsoft.com/en-us/library/windows/desktop/aa375967(v=vs.85).aspx
CCspInformations objCSPs = new CCspInformations();
CX500DistinguishedName objDN = new CX500DistinguishedName();
//top level object that enables installing a certificate response http://msdn.microsoft.com/en-us/library/windows/desktop/aa377809.aspx
CX509Enrollment objEnroll = new CX509Enrollment();
CObjectIds objObjectIds = new CObjectIds();
CObjectId objObjectId = new CObjectId();
CObjectId objObjectId2 = new CObjectId();
CX509ExtensionKeyUsage objExtensionKeyUsage = new CX509ExtensionKeyUsage();
CX509ExtensionEnhancedKeyUsage objX509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage();
string csr_pem = null;
// Initialize the csp object using the desired Cryptograhic Service Provider (CSP)
objCSPs.AddAvailableCsps();
//Provide key container name, key length and key spec to the private key object
objPrivateKey.ProviderName = providerName;
objPrivateKey.Length = KeyLength;
objPrivateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; //Must flag as XCN_AT_KEYEXCHANGE to use this certificate for exchanging symmetric keys (needed for most SSL cipher suites)
objPrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
if (Location == StoreLocation.LocalMachine)
objPrivateKey.MachineContext = true;
else
objPrivateKey.MachineContext = false; //must set this to true if installing to the local machine certificate store
objPrivateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG; //must set this if we want to be able to export it later.
objPrivateKey.CspInformations = objCSPs;
// Create the actual key pair
objPrivateKey.Create();
// Initialize the PKCS#10 certificate request object based on the private key.
// Using the context, indicate that this is a user certificate request and don't
// provide a template name
if (Location == StoreLocation.LocalMachine)
objPkcs10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, objPrivateKey, "");
else
objPkcs10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser, objPrivateKey, "");
//Set hash to sha256
CObjectId hashobj = new CObjectId();
hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID, ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY, AlgorithmFlags.AlgorithmFlagsNone, "SHA256");
objPkcs10.HashAlgorithm = hashobj;
// Key Usage Extension -- we only need digital signature and key encipherment for TLS:
// NOTE: in openSSL, I didn't used to request any specific extensions. Instead, I let the CA add them
objExtensionKeyUsage.InitializeEncode(
CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE |
CERTENROLLLib.X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE
);
objPkcs10.X509Extensions.Add((CX509Extension)objExtensionKeyUsage);
// Enhanced Key Usage Extension
objObjectId.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // OID for Server Authentication usage (see this: http://stackoverflow.com/questions/17477279/client-authentication-1-3-6-1-5-5-7-3-2-oid-in-server-certificates)
objObjectId2.InitializeFromValue("1.3.6.1.5.5.7.3.2"); // OID for Client Authentication usage (see this: http://stackoverflow.com/questions/17477279/client-authentication-1-3-6-1-5-5-7-3-2-oid-in-server-certificates)
objObjectIds.Add(objObjectId);
objObjectIds.Add(objObjectId2);
objX509ExtensionEnhancedKeyUsage.InitializeEncode(objObjectIds);
objPkcs10.X509Extensions.Add((CX509Extension)objX509ExtensionEnhancedKeyUsage);
// Encode the name in using the Distinguished Name object
// see here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa379394(v=vs.85).aspx
objDN.Encode(
Subject,
X500NameFlags.XCN_CERT_NAME_STR_SEMICOLON_FLAG
);
// Assign the subject name by using the Distinguished Name object initialized above
objPkcs10.Subject = objDN;
//suppress extra attributes:
objPkcs10.SuppressDefaults = true;
// Create enrollment request
objEnroll.InitializeFromRequest(objPkcs10);
csr_pem = objEnroll.CreateRequest(
EncodingType.XCN_CRYPT_STRING_BASE64
);
csr_pem = "-----BEGIN CERTIFICATE REQUEST-----\r\n" + csr_pem + "-----END CERTIFICATE REQUEST-----";
return csr_pem;
}
Related
I am developing a video game and I am quite lost in the subject of secure communication between client and server. If anyone can give me a cable I would really appreciate it.
My project is based on a client (c # - Unity) + server (Go) that communicate through a TCP socket. Now I am adding security with TLS 1.2, and although following several articles and posts I have gotten a solution that apparently works, I think that I am not really getting a secure communication since although I change the client's digital certificate for a totally different one and unrelated to the keys everything still works the same, which makes me think that something I have not configured correctly
Regarding the use of RSA keys, I am using the following scheme:
Server: Digital certificate generated with the private key (.pem) + private key to create the socket (.pem)
//// Server Go ////
cert, _ := tls.LoadX509KeyPair(PemCertPath, PrivateKeyPath)
config := tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}}
service := ServerAddress + ":" + strconv.Itoa(ServerPort)
socket, _ := tls.Listen("tcp", service, &config)
Client: Digital certificate generated with the private key (.pem) to connect to the socket.
//// Client C# ////
byte[] certBytes = Convert.FromBase64String("AS9ggSEPw5yp7+IH5S9ingq+.........."); //Cert .pem
X509Certificate2 cert = new X509Certificate2(certBytes);
X509CertificateCollection certCollection = new X509CertificateCollection(new X509Certificate[] { cert });
tcpClient = new TcpClient(serverAddress, serverPort);
sslStreamSerma = new SslStream(tcpClient.GetStream(), false, ValidateCertificate);
sslStreamSerma.AuthenticateAsClient(sermaName, certCollection, SslProtocols.Tls12, false);
I am quite confused with this ... I do not understand what is happening here so that regardless of the digital certificate that the client uses, the communication continues to work ... Should I create 2 pairs of keys, one for the client and one for the server? Should the certificate be generated with the public key?
If anyone can help me I would really appreciate it ...
The problem is that your client certificate doesn't have associated private key which is required to perform client authentication.
byte[] certBytes = Convert.FromBase64String("AS9ggSEPw5yp7+IH5S9ingq+.........."); //Cert .pem
X509Certificate2 cert = new X509Certificate2(certBytes);
this code part reads only public part of the certificate, not private key.
How can we generate a certificate signing request (CSR) on Xamarin platforms.
CertificateRequest can be used for this in desktop .net however Xamarin/mono platforms do not support it, It throws a PlatformNotSupportedException.
The c# version of SandCastle supports creating Certificate Signing Requests (CSR)
byte[] CreateSigningRequest(string commonName, RSA rsa)
{
var name = new X509Name($"C=NZ, O=MyOrg, L=MyLocation, OU=MyOrgUnit, CN={commonName}");
var keys = DotNetUtilities.GetKeyPair(rsa);
Pkcs10CertificationRequest csr = new Pkcs10CertificationRequest("SHA256WITHRSA", name, keys.Public, null, keys.Private);
return csr.GetEncoded();
}
Starting with .Net 4.7.2 (.Net Standard 2.0) it's possible to create self-signed certificates and certificate signing requests with C#/.Net only, see the MS Documentation.
While it's straight forward to create a self-signed certificate which will assert HasPrivateKey (you just call CreateSelfSigned(notBefore, notAfter)) I'm having a hard time to figure out how to get hold of the private key in general, e.g. if I want to create a certificate signed by a CA and then want to persist the certificate as a PFX file or want to persist the private key in a .PEM file or want to store it in the MS certificate store together with the private key, or when I just want to have in memory and also assert HasPrivateKey.
What I do have is a 'RSAParameters' instance which is in possession of the relevant private information, but I failed to figure out how to (easily) use that for the purpose in question (create a PFX file or PEM file or MS Certificate Store entry) without having to read through all the relevant RFCs and write a program for that on my own. (That RSAParameter instance contains the D, Exponent and Modulus, so I could try to patch this together (with the help of this answer, hopefully), but I was hoping for a C# method which will perform these tasks for me (which I could not find) by now).
Of course the idea is to do that with .Net functionality alone, as well.
Every hint on how to achieve this is appreciated.
If you only have Modulus, Exponent, and D you first have to recover the CRT parameters (P, Q, DP, DQ, InverseQ).
As your other questions, you're mainly missing the cert.CopyWithPrivateKey(key) extension methods and rsa.ImportParameters(RSAParameters):
if I want to create a certificate signed by a CA and then want to persist the certificate as a PFX file
using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
using (X509Certificate2 caSigned = GetCASignedCert(rsa))
using (X509Certificate2 withKey = caSigned.CopyWithPrivateKey(rsa))
{
File.WriteAllBytes("some.pfx", withKey.Export(X509ContentType.Pkcs12, "and a password"));
}
}
or want to persist the private key in a .PEM file
This one is available in .NET Core 3.0 daily builds:
RSAParameters rsaParameters = default(RSAParameters);
using (StreamWriter writer = new StreamWriter("rsa.key"))
using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
writer.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
writer.WriteLine(
Convert.ToBase64String(
rsa.ExportRSAPrivateKey(),
Base64FormattingOptions.InsertLineBreaks));
writer.WriteLine("-----END RSA PRIVATE KEY-----");
}
PKCS#8 and encrypted PKCS#8 are also available.
On existing versions this requires using the RSAParameters and a ITU-T X.690 DER encoder.
or want to store it in the MS certificate store together with the private key
using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
using (X509Certificate2 caSigned = GetCASignedCert(rsa))
using (X509Certificate2 withKey = caSigned.CopyWithPrivateKey(rsa))
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
X509Certificate2 persisted = new X509Certificate2(
withKey.Export(X509ContentType.Pkcs12, ""),
"",
X509KeyStorageFlags.PersistKeySet);
using (persisted)
{
store.Open(OpenFlags.ReadWrite);
store.Add(persisted);
}
}
}
or when I just want to have in memory and also assert HasPrivateKey.
using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
using (X509Certificate2 caSigned = GetCASignedCert(rsa))
{
// Yes, this value can outlive both usings
return caSigned.CopyWithPrivateKey(rsa);
}
}
If you are using a certificate hardware security module (HSM) such as a USB key, for example, it is not possible to "get" the private key because the HSM only provides an interface for using the private key. This is for security as once a private key is in a file or memory it is potentially obtainable by a third party.
Also .NET has, historically, not presented a flexible enough interface, although it is improving. As such a many software vendors use the more complete and well-maintained, Bouncy Castle API (http://www.bouncycastle.org/csharp/) and you will find a lot of documentation around the web. Generally if .NET can't do it - bouncy castle will. Ironically an HSM requires .NET crypto access it's private key functionality on windows, but you usually encapsulate that somehow.
It is a steep learning curve using crypto APIs in general and it's unlikely that you will get much assistance without having a code example you want to make work.
I am trying to use pure .net code to create a certificate request and create a certificate from the certificate request against an existing CA certificate I have available (either in the Windows Certificate store or as a separate file).
I know that I have the classes X509Certificate and X509Certificate2 available to load certificates and get access to their information, but I don't see any classes or functionality within the System.Security.Cryptography namespace that could be used to create a certificate request or to sign such a certificate request to create a new signed certificate.
And that although the documentation on the System.Security.Cryptography.Pkcs namespace says:
The System.Security.Cryptography.Pkcs namespace provides programming
elements for Public Key Cryptography Standards (PKCS), including
methods for signing data, exchanging keys, requesting certificates,
public key encryption and decryption, and other security functions.
So, how can I create a certificate request and fulfill that request to create a new X509 certificate using only pure .net classes from System.Security.Cryptography?
Note:
I don't want to use an external executable like openssl or MakeCert
I don't want to use BouncyCastle
I don't want to use Windows Certificate Enrollment API
I don't want to use the native Win32 API functions
Short answer: You can starting in .NET Framework 4.7.2.
This functionality was originally added to .NET Core 2.0 in the form of the CertificateRequest class, which can build a PKCS#10 certification signing request or an X.509 (self-signed or chained) public key certificate.
The classes for that feature were made available in .NET Framework 4.7.2.
using (RSA parent = RSA.Create(4096))
using (RSA rsa = RSA.Create(2048))
{
CertificateRequest parentReq = new CertificateRequest(
"CN=Experimental Issuing Authority",
parent,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
parentReq.CertificateExtensions.Add(
new X509BasicConstraintsExtension(true, false, 0, true));
parentReq.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(parentReq.PublicKey, false));
using (X509Certificate2 parentCert = parentReq.CreateSelfSigned(
DateTimeOffset.UtcNow.AddDays(-45),
DateTimeOffset.UtcNow.AddDays(365)))
{
CertificateRequest req = new CertificateRequest(
"CN=Valid-Looking Timestamp Authority",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
req.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
req.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation,
false));
req.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.8")
},
true));
req.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
using (X509Certificate2 cert = req.Create(
parentCert,
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddDays(90),
new byte[] { 1, 2, 3, 4 }))
{
// Do something with these certs, like export them to PFX,
// or add them to an X509Store, or whatever.
}
}
}
Longer answer if you're stuck on older versions: To accomplish your goal without adding any new P/Invokes, you would need to read and understand the following documents:
ITU-T X.680-201508, the ASN.1 language
IETF RFC 5280 or ITU-T X.509, the documents that explain the fields in X.509 certificates.
IETF RFC 2986, explains the PKCS#10 certification signing request
ITU-T X.690, explains the BER encoding family for ASN.1 (including DER) which tells you how to read and write bytes to achieve the semantic meaning from X.509 / PKCS#10.
And then you could write a DER writer/reader, and just emit the bytes for what you want.
I cant comment on the answer above, so this serves as a comment.
#Keith
if the issue is the private key used for the server certificate is missing the private key i hope this explains whats going on.
To combine the public and private keys in the answer above,
cert = RSACertificateExtensions.CopyWithPrivateKey(cert, rsa);
This will bundle the private key with the certificate for exporting to PFX with
File.WriteAllBytes("filename", cert.Export(X509ContentType.Pfx, "passphrase for export"));
for the example provided above.
The method CreateSelfSigned returns a X509Certificate2 object with the public and private key attached.
Where as when signing against a root, or subordinate
The Create method will only create the public key component in the X509Certificate2 object.
I think this is because the usual certificate methods would use the CSR to sign against and return the public key for acceptance by the client which would never expose the private key to the signing server.
Our organization manages a stable of iOS applications for multiple clients, which means dealing with a lot of different developer identity certificates and push notification certificates.
I have had success with the Bouncy Castle C# Crypto API in simplifying management of the certificates and private keys for push notifications, essentially eliminating the need for the Keychain for all our push notification certificates.
I would like to extend this to the developer identity certificates. The goal would be to store all the private key and certificate information in the database for each developer identity. Then when a new developer or build machine needs to be provisioned, server side code could wrap all of the certificates and private keys into one p12 archive with one password that could be imported into the target Mac's Keychain.
Unfortunately, the Mac Keychain doesn't like the p12 files I'm generating. This is annoying since I can successfully import these files into the Windows certificate manager just fine.
The code I'm using (the important parts) looks like this:
private byte[] GetP12Bytes(List<DevIdentity> identities, string password)
{
Pkcs12Store store = new Pkcs12Store();
foreach(DevIdentity ident in identities)
{
// Easiest to create a Bouncy Castle cert by converting from .NET
var dotNetCert = new X509Certificate2(ident.CertificateBytes);
// This method (not shown) parses the CN= attribute out of the cert's distinguished name
string friendlyName = GetFriendlyName(dotNetCert.Subject);
// Now reconstitute the private key from saved value strings
BigInteger modulus = new BigInteger(ident.PrivateKey.Modulus);
BigInteger publicExponent = new BigInteger(ident.PrivateKey.PublicExponent);
BigInteger privateExponent = new BigInteger(ident.PrivateKey.Exponent);
BigInteger p = new BigInteger(ident.PrivateKey.P);
BigInteger q = new BigInteger(ident.PrivateKey.Q);
BigInteger dP = new BigInteger(ident.PrivateKey.DP);
BigInteger dQ = new BigInteger(ident.PrivateKey.DQ);
BigInteger qInv = new BigInteger(ident.PrivateKey.QInv);
RsaKeyParameters kp = new RsaPrivateCrtKeyParameters(modulus, publicExponent, privateExponent, p, q, dP, dQ, qInv);
AsymmetricKeyEntry privateKey = new AsymmetricKeyEntry(kp);
// Now let's convert to a Bouncy Castle cert and wrap it for packaging
Org.BouncyCastle.X509.X509Certificate cert = DotNetUtilities.FromX509Certificate(dotNetCert);
X509CertificateEntry certEntry = new X509CertificateEntry(cert);
// Set the private key and certificate into the store
store.SetCertificateEntry(friendlyName, certEntry);
store.SetKeyEntry(ident.PrivateKeyName, privateKey, new X509CertificateEntry[] { certEntry });
}
using (MemoryStream ms = new MemoryStream())
{
store.Save(ms, password.ToCharArray(), new SecureRandom());
ms.Flush();
byte[] p12Bytes = ms.ToArray();
return p12Bytes;
}
}
Like I said, this works great for import on Windows, but fails with a very generic error when importing into the Mac Keychain.
There is one major difference I can see when loading a Keychain-generated p12 and my own generated p12 file, but I do not know if this is the cause.
If I load the Mac Keychain generated p12 into a Bouncy Castle PKCS12Store, and then examine the keys, on the Keychain p12, both the certificate and the private key have an attribute with the key "1.2.840.113549.1.9.21" with equivalent values (a DerOctetString with value #af8a1d6891efeb32756c12b7bdd96b5ec673e11e).
If I do the same to my generated p12 file, the private key contains the "1.2.840.113549.1.9.21" attribute, but the Certificate does not.
If I Google "1.2.840.113549.1.9.21", I find out that this OID means PKCS_12_LOCAL_KEY_ID . My only theory is that the Keychain relies on this to match up the certificate and private key, and that my generated file does not have this, so it fails.
However, I've tried adding these values to a Hashtable and then using the CertificateEntry constructor that takes the attribute hashtable. If I do that, and then save the bytes, and then reload the bytes, that attribute is again missing.
So I'm flummoxed. Maybe this attribute is a glitch in the Bouncy Castle API? Maybe there's something I'm doing wrong. Maybe the Keychain has ridiculous non-standard requirements for incoming p12 files. In any case, any help that could be provided would be greatly appreciated.
BouncyCastle's Pkcs12Store takes care of setting both the Friendly Name and Local Key ID attributes for you (or at least it does so in the 1.7 release, circa April 2011). My guess is that you must have used an older version where this didn't work.
Here's how I'm saving an iPhone Developer identity to a Pkcs12Store instance (extra stuff and security omitted):
var store = new Pkcs12Store();
// pairs is IEnumerable<Tuple<X509Certificate, AsymmetricKeyParameter>>
foreach (var pair in pairs)
{
var cn = pair.Item1.SubjectDN
.GetValueList(X509Name.CN).OfType<string>().Single();
var certEntry = new X509CertificateEntry(pair.Item1);
store.SetCertificateEntry(cn, certEntry);
var keyEntry = new AsymmetricKeyEntry(pair.Item2);
store.SetKeyEntry("Developer Name", keyEntry, new[] { certEntry });
}
store.Save(stream, string.Empty.ToArray(), new SecureRandom());
Importing the store in Keychain Access.app on OS X 10.7 correctly places the certificate and private key in the keychain and places the certificate within the private key in the UI, as with a certificate and key generated by Keychain Access itself.
On a side note, it seems that Pkcs12Store uses the public key of the certificate to generate the value of the LocalKeyId attribute shared by the certificate and key entries.
You can see the relevant section of the Pkcs12Store source here.