Adding trust for a X509 CA certificate imported into keychain on OS X - objective-c

Recently I wrote a little chunk of code that grabs a CA certificate from a SCEP server, turns it into a SecCertificateRef and adds it to a keychain (either System or login). Now I'm wondering how I can get the system to trust that certificate. I've been playing around with Trust Policies but I haven't had much luck yet.
On top of this, I understand that the system may not allow you to automatically trust a certificate without user interaction. If that's the case, how do you kick off the interaction? Using "SecCertificateAddToKeychain" puts the certificate into the keychain silently.
Side note: I'm trying to support 10.5 with this code as well.
Thanks for any help!
Edit:
After playing around with the code on the Citrix page I came up with my own function. From what I gathered from the Citix page, this method is destructive. So if the certificate is already in the keychain and already has policies (iChat, etc) this will overwrite those. Since I don't care about that in my project, here's a simpler version I came up with.
-(OSStatus) addCertificate: (CertificateWrapper *) cert trust:(BOOL) shouldTrust {
//keychain is a SecKeychainRef created with SecKeychainOpen
OSStatus result = SecCertificateAddToKeychain([cert certificate], keychain);
if((result == noErr || result == errKCDuplicateItem) && shouldTrust){
SecTrustSettingsDomain domains[3] = { kSecTrustSettingsDomainSystem, kSecTrustSettingsDomainAdmin, kSecTrustSettingsDomainUser};
for(int i = 0; i < 3; i++){
CFMutableArrayRef trustSettingMutArray = NULL;
trustSettingMutArray = CFArrayCreateMutable (NULL, 0, &kCFTypeArrayCallBacks);
result = SecTrustSettingsSetTrustSettings([cert certificate], domains[i], trustSettingMutArray );
if(result == noErr){
break;
}
}
}
return result;
}

There is a great example of how to do this on the Citrix web site with a ton of sample code.

Related

Keyset does not exist /r /n

I was tasked to create a integration service between our SharePoint app and one service provider. One requirement of the service provider I'm going to integrate with is to provide them a public key which they will use to verify my request which was signed using our own private key.
Initially I created a console app which reads the certificate store and gets the private key which to use to sign my request and all. The console app works fine so I decided to move it now within our SharePoint application. Unfortunately it fails in this specific part of the code:
key.FromXmlString(privateCert.PrivateKey.ToXmlString(true));
The whole code snippet which gets the certificate and does the signing can be found below:
X509Certificate2 privateCert = null;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.MaxAllowed);
var certs = store.Certificates.Find(X509FindType.FindByThumbprint, "thumbprinthere", true);
if (certs.Count > 0)
{
privateCert = certs[0];
}
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.FromXmlString(privateCert.PrivateKey.ToXmlString(true));
byte[] sig = key.SignData(Encoding.ASCII.GetBytes(data), CryptoConfig.MapNameToOID("SHA256"));
string signature = Convert.ToBase64String(sig);
[UPDATE]
I tried following the steps in this link. I first uninstalled my existing private key in the server. I then imported it back to the Certificate store and confirmed that there was a Thumbprint property. After that, I ran findprivatekey.exe and was able to navigate to the MachineKeys folder. From there I added different users ranging from Network Services, IIS_IUSRS and even local accounts I used to login to the server as well as SPFarm admin but I still keep getting the error.
I also made sure that the key I added was exportable so there should be a way for it the application to extract the private key attached to the certificate.
[UPDATE 2]
I updated the code so that it just returns one certificate prior to assigning it to the variable I was using to extract the private key. Still the same issue even if I can see that the certs variable is returning exactly one record.
After much checking I realized I missed one important part in calling the method code block above. I forgot to wrap it an elevate privilege block. After doing that, the code functioned similarly as my console app.
SPSecurity.RunWithElevatedPrivileges(delegate())
{
...
X509Certificate2 privateCert = null;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.MaxAllowed);
var certs = store.Certificates.Find(X509FindType.FindByThumbprint, "<thumbprinthere>", true);
if (certs.Count > 0)
{
privateCert = certs[0];
}
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.FromXmlString(privateCert.PrivateKey.ToXmlString(true));
byte[] sig = key.SignData(Encoding.ASCII.GetBytes(data), CryptoConfig.MapNameToOID("SHA256"));
string signature = Convert.ToBase64String(sig);
...
});

User login with Smart Card for Windows UWP app

This seems like such a simple thing but I have been trying to figure this out for over a week now and cannot seem to figure it out. We are creating a Windows UWP app using WinJS and would like the user to login to the app with a PIV (smart card)/PIN combination. Essentially, when the app starts it will verify that there is a smart card inserted into the device and then prompt the user for the PIN. If the PIN is validated against the smart card the app will log the user in.
We do have Windows 7 applications that currently do this and I attempted to convert that code however it appears the APIs we used are not valid for Windows UWP apps. I did post the question about those APIs but did not receive any responses (https://stackoverflow.com/questions/43344679/x509certificate2ui-class-equivalent-with-windows-uwp-and-winjs). With Windows 7 we used the X509Certificate2UI (https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2ui(v=vs.110).aspx ) class to select a certificate which prompted the user for the PIN.
After a lot of research, I believe (and could be wrong) with Windows UWP I need to use the smart card APIs (https://learn.microsoft.com/en-us/uwp/api/windows.devices.smartcards ). I have been reading the past couple of days and went through several Microsoft documents on smart cards like this one: https://learn.microsoft.com/en-us/windows/uwp/security/smart-cards but have not been able to find a way to validate a user entered PIN against the PIN on the smart card.
From the SmartCardProvisioning class (https://learn.microsoft.com/en-us/uwp/api/windows.devices.smartcards.smartcardprovisioning ) we are able to call the requestPinChangeAsync() method which prompts the user for the current PIN and the new PIN. I am looking for similar functionality except that it only asks for the current PIN and then returns a value that will let the app know if the PIN was correct.
I have also read through Microsoft’s Hello (https://learn.microsoft.com/en-us/windows/uwp/security/microsoft-passport ) API but did not see a way to use it with smart cards.
Can anyone point me in the right direction on how to use two-factor authentication in my app using a smart card/PIN combination. It seems like I have been in a Google bubble for the past several days going round and round and need help to get out.
Thanks
edit to explain why it is not a duplicate:
Not really a duplicate, both questions were asked by me and I mention the other post in the bod of the question. In the other post I was looking for an equivalent to the X509Certificate2UI class for Windows UWP with WinJS. With further research, I am thinking that might not be the correct way to go therefore with this post I am looking to see if anyone can point me in the right direction to doing two-factor authentication using a PIV (smart card) and the PIN associated with the card.
EDIT: Share code that works:
Here is the WinJS code that seems to work. Not sure is there is a better way or not:
if (certToUse != null) {
Windows.Security.Cryptography.Core.PersistedKeyProvider.openKeyPairFromCertificateAsync(certToUse, Windows.Security.Cryptography.Core.HashAlgorithmNames.sha256, Windows.Security.Cryptography.Core.CryptographicPadding.rsaPkcs1V15).then(function (keyPair) {
var buffer = 'data to sign'
var data = Windows.Security.Cryptography.CryptographicBuffer.convertStringToBinary(buffer, Windows.Security.Cryptography.BinaryStringEncoding.utf16BE)
Windows.Security.Cryptography.Core.CryptographicEngine.signAsync(keyPair, data).then(function (signed) {
var results = Windows.Security.Cryptography.Core.CryptographicEngine.verifySignature(keyPair, data, signed)
completeValidatePin = true
successCallback(true)
}, function (reason) {
completeValidatePin = true
errorCallback('User cancelled login')
})
}, function (reason) {
completeValidatePin = true
errorCallback('Error using certificate')
})
} else {
errorCallback('Certificate not found')
}
I'm currently investigating your question and trying to determine if there is a good solution.
I did write the following code which I thought should work:
IReadOnlyList<Certificate> Certs;
CertificateQuery CertQuery = new CertificateQuery();
CertQuery.HardwareOnly = true;
Certs = await CertificateStores.FindAllAsync(CertQuery);
string strEncrypt = "test";
IBuffer BufferToEncrypt = CryptographicBuffer.ConvertStringToBinary(strEncrypt, BinaryStringEncoding.Utf8);
foreach (Certificate Cert in Certs)
{
if (Cert.HasPrivateKey && ((Cert.KeyStorageProviderName == "Microsoft Base Smart Card Crypto Provider") || Cert.KeyStorageProviderName == "Microsoft Smart Card Key Storage Provider"))
{
CryptographicKey Key = null;
try
{
Key = await PersistedKeyProvider.OpenKeyPairFromCertificateAsync(Cert, HashAlgorithmNames.Sha1, CryptographicPadding.RsaPkcs1V15);
}
catch (Exception ex)
{
// Could not open Smart Card Key Pair
}
if (Key != null)
{
try
{
// Try to Sign with Cert Private key
IBuffer EncryptedBuffer = CryptographicEngine.Sign(Key, BufferToEncrypt);
}
catch (Exception ex)
{
// Could not sign
}
}
}
}
Unfortunately, OpenKeyPairFromCertificateAsync creates a provider with a silent context so CryptographicEngine.Sign is unable to display a PIN dialog. I will have to look into it a bit more.

Authorization session always ask for user password in helper

I have an Objective-C application (https://github.com/NBICreator/NBICreator) with a privileged helper tool.
I have a few different privileged tasks the helper will need to perform during one build, and I want to have the user authenticate only once to perform those tasks.
The authorization works, but I can't seem to reuse the session in the helper. The user always have to authenticate for every step, even if I supply the exact same right to the Security Server and use the same AuthenticationRef.
I have read the docs and tested the pre-authentication methods in the main app first, and tried printing out (and retaining the auth session in the helper as well). But nothing I've tried have yet to work successfully.
I need som help figuring out why the Security Server feel the need to reauthenticate.
The code on GitHub in the Master Branch is current, and what I'm trying and testing by changing things back and forth. With that code, the user have to authenticate each time I call a helper function, even if I use the same authentication right.
This is what the right looks like in the authorization database:
class = rule;
created = "470329367.933364";
"default-prompt" = {
"" = "NBICreator is trying to start an Imagr workflow.";
};
identifier = "com.github.NBICreator";
modified = "470329367.933364";
requirement = "identifier \"com.github.NBICreator\" and anchor apple generic and certificate leaf[subject.CN] = \"Mac Developer: Erik Berglund (BXUF2UUW7E)\" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */";
rule = (
"authenticate-admin"
);
version = 0;
This is where I define the right: https://github.com/NBICreator/NBICreator/blob/master/NBICreator/Helper/NBCHelperAuthorization.m#L36-L43
NSStringFromSelector(#selector(authorizeWorkflowImagr:withReply:)) : #{
kCommandKeyAuthRightName : #"com.github.NBICreator.workflowImagr",
kCommandKeyAuthRightDefault : #kAuthorizationRuleAuthenticateAsAdmin,
kCommandKeyAuthRightDesc : NSLocalizedString(
#"NBICreator is trying to start an Imagr workflow.",
#"prompt shown when user is required to authorize to add a user"
)
},
And this is where I check if the user is authenticated:
https://github.com/NBICreator/NBICreator/blob/master/NBICreator/Helper/NBCHelperAuthorization.m#L222-L253
+ (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command authRef:(AuthorizationRef)authRef {
#pragma unused(authData)
NSError * error;
OSStatus err = 0;
AuthorizationItem oneRight = { NULL, 0, NULL, 0 };
AuthorizationRights rights = { 1, &oneRight };
oneRight.name = [#"com.github.NBICreator.workflowImagr" UTF8String];
err = AuthorizationCopyRights(
authRef,
&rights,
NULL,
kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed,
NULL
);
if ( err != errAuthorizationSuccess ) {
NSString *message = CFBridgingRelease(SecCopyErrorMessageString(err, NULL));
error = [NSError errorWithDomain:[[NSProcessInfo processInfo] processName] code:err userInfo:#{ NSLocalizedDescriptionKey : message }];
}
return error;
}
As you can see there, I'm testing by setting a hardcoded right name to try and resue that right to the Security Server.
I'm stumped right now, and can't seem to find a way forward. Hoping someone here might know where to look.
I found the answer in how I set up the right in the rights database.
I was using the default code from the Apple example EvenBetterAuthorizationExample for creating and using the rights. But that only points to this documentation for the different rights to use.
None of those were helping me with authenticatin once and the having the helper be authenticated the next time I request authentication for the same right.
After some digging and looking into the actual rules in the authorization database I found a way to copy the rule that AuthenticateWithPrivileges uses, that works by authenticating for 5 minutes until requiring re-authentication.
So, I changed my code for creating my custom right from this:
NSStringFromSelector(#selector(addUsersToVolumeAtPath:userShortName:userPassword:authorization:withReply:)) : #{
kCommandKeyAuthRightName : NBCAuthorizationRightAddUsers,
kCommandKeyAuthRightDefault : #kAuthorizationRuleAuthenticateAsAdmin,
kCommandKeyAuthRightDesc : NSLocalizedString(
#"NBICreator is trying to add a user.",
#"prompt shown when user is required to authorize to add a user"
)
},
To this:
NSStringFromSelector(#selector(addUsersToVolumeAtPath:userShortName:userPassword:authorization:withReply:)) : #{
kCommandKeyAuthRightName : NBCAuthorizationRightAddUsers,
kCommandKeyAuthRightDefault : #{
#"class": #"user",
#"group": #"admin",
#"timeout": #(300),
#"version": #(1),
},
kCommandKeyAuthRightDesc : NSLocalizedString(
#"NBICreator is trying to add a user.",
#"prompt shown when user is required to authorize to add a user"
)
},
So, instead of using #kAuthorizationRuleAuthenticateAsAdmin as the RightDefault, i passed in this dict:
#{
#"class": #"user",
#"group": #"admin",
#"timeout": #(300),
#"version": #(1),
},
After that, my helper can request authorization from the user, and then I can reuse that session for the same right during 5 minutes without asking the user again.

load SSL CA's from string as opposed to from file

I'd like to store my CAs in a string inside my binary instead of loading it in via SSL_CTX_load_verify_locations, which takes in file paths and folder paths.
However, I can't find a method that lets me take in a string.
I don't know of a documented way to do this. The only way I know of is to roll your own verification, e.g. create a store using X509_STORE_CTX_new, add the trusted CAs with X509_STORE_CTX_trusted_stack, add the certificate with X509_STORE_CTX_set_cert add some other chain certificates and CRLs with similar function and finally call X509_verify_cert on the X509_STORE_CTX.
OK I figured out how to do it. OpenSSL has a bunch of ways to deal with loading certs, and many of them add the certs to the non-trusted chain. You must use SSL_CTX_get_cert_store and X509_STORE_add_cert in conjunction. These two functions take in X509 pointers, which can be created from a raw c string. Since the documentation is pretty much non-existent for these two functions, so I figured I'd share the code here:
Edit: this is basically Steffen Ulrich's method except using X509_STORE_add_cert.
#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/x509.h>
void read_cert_into_ctx(istream &some_stream, SSL_CTX *ctx) {
// Add a stream of PEM formatted certificate strings to the trusted store
// of the ctx.
string line;
string buffer;
while(getline(some_stream, line)) {
buffer.append(line);
buffer.append("\n");
if(line == "-----END CERTIFICATE-----") {
BIO *bio;
X509 *certificate;
bio = BIO_new(BIO_s_mem());
BIO_puts(bio, buffer.c_str());
certificate = PEM_read_bio_X509(bio, NULL, 0, NULL);
if(certificate == NULL)
throw std::runtime_error("could not add certificate to trusted\
CAs");
X509_STORE* store = SSL_CTX_get_cert_store(ctx);
int result = X509_STORE_add_cert(store, certificate);
BIO_free(bio);
buffer = "";
}
}
}

Set callback for System.DirectoryServices.DirectoryEntry to handle self-signed SSL certificate?

I have an application replicating data from a directory service using typical System.DirectoryServices.DirectoryEntry code. I now have a requirement to replicate from Novell eDirectory using SSL with a self-signed certificate. I suspect that the existing code would work with a valid certificate that could be verified, or perhaps if the self-signed cert is added to the local machine keystore. In order to make it work for sure with a self-signed cert however, the only solution I can find is to use the System.DirectoryServices.Protocols namespace and the LdapConnection class, whereby I can wire up a VerifyServerCertificate callback. I can't find any way of applying the same concept to a DirectoryEntry instance, or of connecting with an LdapConnection instance and somehow "converting" that to a DirectoryEntry instance. Maybe it isn't possible, I'd just like to confirm that really. Any other thoughts welcome.
The only pertinent link I've found is at: http://www.codeproject.com/Articles/19097/eDirectory-Authentication-using-LdapConnection-and
This is a phenomenal question.
I've been battling this same issue for a few days now, and I've finally got some definitive proof on why the DirectoryEntry object will not work in this scenario.
This particular Ldap server (running on LDAPS 636) also issues it's own self signed certificate. Using LdapConnection (and monitoring the traffic via Wireshark), I noticed a handshake taking place that does not occur when using DirectoryEntry :
The first sequence is the from the secured ldap server, the second sequence is from my machine. The code that prompts the second sequence is :
ldapConnection.SessionOptions.VerifyServerCertificate += delegate { return true; };
There are others way to "fake out" the callback, but this what I've been using.
Unfortunately, DirectoryEntry does not have an option or method to verify a self signed cert, thus the acceptance of the certificate never happens (second sequence), and the connection fails to initialize.
The only feasible way to accomplish this is by using LdapConnection, in conjunction with a SearchRequest and SearchResponse. This is what I've got so far :
LdapConnection ldapConnection = new LdapConnection("xxx.xxx.xxx:636");
var networkCredential = new NetworkCredential("Hey", "There", "Guy");
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.SessionOptions.VerifyServerCertificate += delegate { return true; };
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(networkCredential);
SearchRequest request = new SearchRequest("DC=xxx,DC=xxx,DC=xxx", "(sAMAccountName=3074861)", SearchScope.Subtree);
SearchResponse response = (SearchResponse)ldapConnection.SendRequest(request);
if(response.Entries.Count == 1)
{SearchResultEntry entry = response.Entries[0];
string DN = entry.DistinguishedName;}
From there you can gather AD Properties from the SearchResponse, and process accordingly. This is a total bummer though, because the SearchRequest seems to be much slower then using the DirectoryEntry.
Hope this helps!
I promise, this will be my last post on this particular question. :)
After another week of research and development, I have a nice solution to this, and it has worked exceedingly well for me thus far.
The approach is somewhat different then my first answer, but in general, it's the same concept; using the LdapConnection to force validation of the certificate.
//I set my Domain, Filter, and Root-AutoDiscovery variables from the config file
string Domain = config.LdapAuth.LdapDomain;
string Filter = config.LdapAuth.LdapFilter;
bool AutoRootDiscovery = Convert.ToBoolean(config.LdapAuth.LdapAutoRootDiscovery);
//I start off by defining a string array for the attributes I want
//to retrieve for the user, this is also defined in a config file.
string[] AttributeList = config.LdapAuth.LdapPropertyList.Split('|');
//Delcare your Network Credential with Username, Password, and the Domain
var credentials = new NetworkCredential(Username, Password, Domain);
//Here I create my directory identifier and connection, since I'm working
//with a host address, I set the 3rd parameter (IsFQDNS) to false
var ldapidentifier = new LdapDirectoryIdentifier(ServerName, Port, false, false);
var ldapconn = new LdapConnection(ldapidentifier, credentials);
//This is still very important if the server has a self signed cert, a certificate
//that has an invalid cert path, or hasn't been issued by a root certificate authority.
ldapconn.SessionOptions.VerifyServerCertificate += delegate { return true; };
//I use a boolean to toggle weather or not I want to automatically find and query the absolute root.
//If not, I'll just use the Domain value we already have from the config.
if (AutoRootDiscovery)
{
var getRootRequest = new SearchRequest(string.Empty, "objectClass=*", SearchScope.Base, "rootDomainNamingContext");
var rootResponse = (SearchResponse)ldapconn.SendRequest(getRootRequest);
Domain = rootResponse.Entries[0].Attributes["rootDomainNamingContext"][0].ToString();
}
//This is the filter I've been using : (&(objectCategory=person)(objectClass=user)(&(sAMAccountName={{UserName}})))
string ldapFilter = Filter.Replace("{{UserName}}", UserName);
//Now we can start building our search request
var getUserRequest = new SearchRequest(Domain, ldapFilter, SearchScope.Subtree, AttributeList);
//I only want one entry, so I set the size limit to one
getUserRequest.SizeLimit = 1;
//This is absolutely crucial in getting the request speed we need (milliseconds), as
//setting the DomainScope will suppress any refferal creation from happening during the search
SearchOptionsControl SearchControl = new SearchOptionsControl(SearchOption.DomainScope);
getUserRequest.Controls.Add(SearchControl);
//This happens incredibly fast, even with massive Active Directory structures
var userResponse = (SearchResponse)ldapconn.SendRequest(getUserRequest);
//Now, I have an object that operates very similarly to DirectoryEntry, mission accomplished
SearchResultEntry ResultEntry = userResponse.Entries[0];
The other thing I wanted to note here is that SearchResultEntry will return user "attributes" instead of "properties".
Attributes are returned as byte arrays, so you have to encode those in order to get the string representation. Thankfully, System.Text.Encoding contains a native ASCIIEncoding class that can handle this very easily.
string PropValue = ASCIIEncoding.ASCII.GetString(PropertyValueByteArray);
And that's about it! Very happy to finally have this figured out.
Cheers!
I have used below code to connect with ldaps using DirectoryEntry.
What i understood in my scenerio is directoryEntry does not work when ldaps is specified in server path or authentication type is mentioned as "AuthenticationTypes.SecureSocketsLayer" but if only ldaps port is mentioned at the end of server name it work. After having a look at wireshark log i can see handshake taking place as mentioned in above post.
Handshake:
Code:
public static SearchResultCollection GetADUsers()
{
try
{
List<Users> lstADUsers = new List<Users>();
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://adserver.local:636", "username", "password");
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.PropertiesToLoad.Add("samaccountname");
SearchResult result;
SearchResultCollection resultCol = search.FindAll();
Console.WriteLine("Record count " + resultCol.Count);
return resultCol;
}
catch (Exception ex)
{
Console.WriteLine("exception" + ex.Message);
return null;
}
}