Авторизация с помощью клиентских SSL сертификатов в IOS и Android
Протокол безопасной передачи данных SSL (Secure Sockets Layer) помимо обеспечения безопасной передачи данных так же позволяет реализовать авторизацию клиентов при помощи клиентских SSL сертификатов. Данная статья является практическим руководством по реализации данного вида авторизации в мобильных приложениях на IOS и Android.Процесс организации работы сервера обеспечивающего такой вид авторизации в статье не рассматривается, однако в конце приведены ссылки по данной тематике.
Процесс авторизации выглядит следующим образом. При переходе клиента в закрытую область сервер запрашивает у клиента сертификат, если проверка прошла успешно то клиент получает доступ к закрытому контенту в ином случае клиент может получить ошибку «No required SSL certificate was sent».Для организации подключения мы сгенирировали клиентский сертификат, а так же создали запрос на подписание сертификата в результате чего получили файл client.csr. Далее мы отправили данный файл поставщику услуг и получили наш подписанный клиентский сертификат необходимый для аутентификации на удаленном сервере.
Тестирование подключения может быть осуществлено при помощи утилиты curl.
curl cert client.crt key client.key k someserive.com/
Однако стоит заметить, что в последняя версия curl 7.30.0 в OS X сломана и не может быть использована для организации тестирования (http://curl.haxx.se/mail/archive-2013–10/0036.html).
Для передачи клиентского сертификата мы будем использовать файл в формате PKCS#12. В файлах PKCS#12 хранятся одновременно и закрытый ключ, и сертификат (разумеется в зашифрованном виде). Примерная организация PKCS#12 файла показана на рисунке.
Сконвертировать Ваш client.crt в файл формата PKCS#12 можно при помощи следующей команды:
openssl pkcs12 export in client.crt inkey client.key out client.p12
После того как мы получили файл в формате PKCS#12 можно переходить к разработке и тестированию нашего мобильного приложения. Начнем с IOS.
1. Реализуем IOS версию приложения Необходимо подключить к Вашему проекту Security.FrameworkДля осуществления запроса нам необходимо извлечеть из PKCS#12 цифровой сертификат и ассоциированный с ним приватный ключ (SecIdentityRef). Наличие данного объекта позволит нам получить соответствующий NSURLCredential.Итак реализуем функецию extractIdentityAndTrust. OSStatus extractIdentityAndTrust (CFDataRef inP12data, SecIdentityRef *identity) { OSStatus securityError = errSecSuccess; CFStringRef password = CFSTR (»); const void *keys[] = { kSecImportExportPassphrase }; const void *values[] = { password }; CFDictionaryRef options = CFDictionaryCreate (NULL, keys, values, 1, NULL, NULL); CFArrayRef items = CFArrayCreate (NULL, 0, 0, NULL); securityError = SecPKCS12Import (inP12data, options, &items); if (securityError == 0) { CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0); const void *tempIdentity = NULL; tempIdentity = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity); *identity = (SecIdentityRef)tempIdentity; } if (options) { CFRelease (options); } return securityError; } Производим извлечение при помощи функции SecPKCS12Import, незабываем указать пароль к серитификату.Далее реализуем делегат canAuthenticateAgainstProtectionSpace, вызов данного делегата позволяет нам определить свойства сервера, а именно протокол, механизм авторизации. У нас реализация этого делегата будет простой, укажем, что обрабатываем любой способ аутентификации представленный сервером. — (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { return YES; } Обработаем возможные ошибки: — (void)connection:(NSURLConnection*) connection didFailWithError:(NSError *)error { NSLog (@«Did recieve error: %@», [error localizedDescription]); NSLog (@»%@», [error userInfo]); } Теперь перейдем к реализации непосредственно механизма аутентификации. Реализуем делегат didRecieveAuthentificationChallenge: — (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { NSLog (@«Authentication challenge»); // load cert NSString *path = [[NSBundle mainBundle] pathForResource:@«keystore» ofType:@«p12»]; NSData *p12data = [NSData dataWithContentsOfFile: path]; CFDataRef inP12data = (__bridge CFDataRef)p12data; SecIdentityRef myIdentity; OSStatus status = extractIdentityAndTrust (inP12data, &myIdentity); SecCertificateRef myCertificate; SecIdentityCopyCertificate (myIdentity, &myCertificate); const void *certs[] = { myCertificate }; CFArrayRef certsArray = CFArrayCreate (NULL, certs, 1, NULL); NSURLCredential *credential = [NSURLCredential credentialWithIdentity: myIdentity certificates:(__bridge NSArray*)certsArray persistence: NSURLCredentialPersistenceForSession]; [[challenge sender] useCredential: credential forAuthenticationChallenge: challenge]; } Загружаем наш сертификат, извлекаем из него нужные нам данные, создаем NSURLCredential, передаем нужную информацию, сохраняем данные аутентификации только в контексте текущей сессии.Ну и для полноты картины приведу код подготавливающий NSURLConnection:
NSString *key = @«test»; NSError *jsonSerializationError = nil; NSMutableDictionary *projectDictionary = [NSMutableDictionary dictionaryWithCapacity:1]; [projectDictionary setObject: key forKey:@«test»]; NSData *jsonData = [NSJSONSerialization dataWithJSONObject: projectDictionary options: nil error:&jsonSerializationError]; NSURL *requestUrl = [[NSURL alloc] initWithString:@«https://your_service»]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: requestUrl cachePolicy: NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0]; [request setHTTPMethod:@«POST»]; [request setValue:@«UTF-8» forHTTPHeaderField:@«content-charset»]; [request setValue:@«application/json» forHTTPHeaderField:@«Accept»]; [request setValue:@«application/json» forHTTPHeaderField:@«Content-Type»]; [request setValue:[NSString stringWithFormat:@»%d», [jsonData length]] forHTTPHeaderField:@«Content-Length»]; [request setHTTPBody: jsonData]; NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest: request delegate: self]; [connection start]; Реализацию делегата didReceiveData приводить не буду.
2. Реализуем Android версию приложения Начну сразу с кода: KeyStore keystore = KeyStore.getInstance («PKCS12»); keystore.load (getResources ().openRawResource (R.raw.keystore),».toCharArray ()); SSLSocketFactory sslSocketFactory = new AdditionalKeyStoresSSLSocketFactory (keystore); HttpParams params = new BasicHttpParams (); HttpProtocolParams.setVersion (params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset (params, HTTP.UTF_8); HttpProtocolParams.setUseExpectContinue (params, true); final SchemeRegistry registry = new SchemeRegistry (); registry.register (new Scheme («http», PlainSocketFactory.getSocketFactory (), 80)); registry.register (new Scheme («https», sslSocketFactory, 3123)); ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager (params, registry); DefaultHttpClient httpclient = new DefaultHttpClient (manager, params); HttpPost httpPostRequest = new HttpPost («https://your_service»); // datas — array which contains data to send to server StringEntity se = new StringEntity (datas[0].toString (), HTTP.UTF_8); // Set HTTP parameters httpPostRequest.setEntity (se); httpPostRequest.setHeader («Accept», «application/json»); httpPostRequest.setHeader («Content-Type», «application/json»); HttpResponse response = httpclient.execute (httpPostRequest); Получаем экземпляр соответствующего KeyStore в нашем случае это (PKCS12), загружаем из ресурсов наш сертификат, вторым аргументом указываем пароль. Далее создаем экземпляр SSLSocketFactory, использую собственную реализацию SSLSocketFactory, позволяющую инициализировать SSL контекст с использованием нашего сертификата. Код фабрики приведен чуть ниже. Далее конфигурируем параметры подключения, регистрируем нашу фабрику, указываем порт на который будем посылать запрос, формируем соответсвующий POST и выполняем запрос.Код фабрики:
import java.io.IOException; import java.net.Socket; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.SSLSocketFactory;
/** * Allows you to trust certificates from additional KeyStores in addition to * the default KeyStore */ public class AdditionalKeyStoresSSLSocketFactory extends SSLSocketFactory { protected SSLContext sslContext = SSLContext.getInstance («TLS»);
public AdditionalKeyStoresSSLSocketFactory (KeyStore keyStore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { super (null, null, null, null, null, null); KeyManagerFactory kmf = KeyManagerFactory.getInstance (KeyManagerFactory.getDefaultAlgorithm ()); kmf.init (keyStore,».toCharArray ()); sslContext.init (kmf.getKeyManagers (), new TrustManager[]{new ClientKeyStoresTrustManager (keyStore)}, new SecureRandom ()); }
@Override public Socket createSocket (Socket socket, String host, int port, boolean autoClose) throws IOException { return sslContext.getSocketFactory ().createSocket (socket, host, port, autoClose); }
@Override public Socket createSocket () throws IOException { return sslContext.getSocketFactory ().createSocket (); }
/** * Based on http://download.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#X509TrustManager */ public static class ClientKeyStoresTrustManager implements X509TrustManager {
protected ArrayList
protected ClientKeyStoresTrustManager (KeyStore… additionalkeyStores) {
final ArrayList
try { // The default Trustmanager with default keystore final TrustManagerFactory original = TrustManagerFactory.getInstance (TrustManagerFactory.getDefaultAlgorithm ()); original.init ((KeyStore) null); factories.add (original);
for (KeyStore keyStore: additionalkeyStores) { final TrustManagerFactory additionalCerts = TrustManagerFactory.getInstance (TrustManagerFactory.getDefaultAlgorithm ()); additionalCerts.init (keyStore); factories.add (additionalCerts); }
} catch (Exception e) { throw new RuntimeException (e); }
/* * Iterate over the returned trustmanagers, and hold on * to any that are X509TrustManagers */ for (TrustManagerFactory tmf: factories) for (TrustManager tm: tmf.getTrustManagers ()) if (tm instanceof X509TrustManager) x509TrustManagers.add ((X509TrustManager) tm);
if (x509TrustManagers.size () == 0) throw new RuntimeException («Couldn’t find any X509TrustManagers»);
}
public void checkClientTrusted (X509Certificate[] chain, String authType) throws CertificateException { for (X509TrustManager tm: x509TrustManagers) { try { tm.checkClientTrusted (chain, authType); return; } catch (CertificateException e) { } } throw new CertificateException (); }
/* * Loop over the trustmanagers until we find one that accepts our server */ public void checkServerTrusted (X509Certificate[] chain, String authType) throws CertificateException { for (X509TrustManager tm: x509TrustManagers) { try { tm.checkServerTrusted (chain, authType); return; } catch (CertificateException e) { } } throw new CertificateException (); }
public X509Certificate[] getAcceptedIssuers () {
final ArrayList
} Заключение. Мы рассмотрели как производить авторизацию по SSL с использованием клиентского сертификата.Полезная информация: Certificate, Key, and Trust Services Tasks for iOSОписание PKCS12Creating .NET web service with client certificate authenticationCertificate Authentication in asp.netJava 2-way TLS/SSL (Client Certificates) and PKCS12 vs JKS KeyStores
Спасибо за внимание!