Как стать автором
Обновить

Авторизация с помощью клиентских SSL сертификатов в IOS и Android

Время на прочтение 8 мин
Количество просмотров 37K
Протокол безопасной передачи данных 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<X509TrustManager> x509TrustManagers = new ArrayList<X509TrustManager>();

        protected ClientKeyStoresTrustManager(KeyStore... additionalkeyStores) {
            final ArrayList<TrustManagerFactory> factories = new ArrayList<TrustManagerFactory>();

            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<X509Certificate> list = new ArrayList<X509Certificate>();
            for ( X509TrustManager tm : x509TrustManagers )
                list.addAll(Arrays.asList(tm.getAcceptedIssuers()));
            return list.toArray(new X509Certificate[list.size()]);
        }
    }

}

Заключение.

Мы рассмотрели как производить авторизацию по SSL с использованием клиентского сертификата.

Полезная информация:
Certificate, Key, and Trust Services Tasks for iOS
Описание PKCS12
Creating .NET web service with client certificate authentication
Certificate Authentication in asp.net
Java 2-way TLS/SSL (Client Certificates) and PKCS12 vs JKS KeyStores

Спасибо за внимание!
Теги:
Хабы:
+15
Комментарии 8
Комментарии Комментарии 8

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн