четверг, 4 октября 2012 г.

Android. HTTPS-соединение с клиентским сертификатом

 Поступила мне задачка: Написать приложение под Андройд, которое ходит с клиентским сертификатом через https соединение. Вроде, что может быть проще! Снабжать свои приложения возможностью выхода в сеть я научилась еще давно. И думала, что легко будет сделать тоже самое, только еще и с сертификатом. Но не тут-то было. Долго искала примеры, читала различные доки. Простое https-соединение у меня получалось. Как и думала - нет ничего проще. Но заставить приложение использовать клиентский сертификат....оказалось сложнее. Это сейчас я знаю, что все на самом деле легко. Надо googl'у  задавать правильные вопросы и в различных примерах убирать все ненужные " красявости". В самом начале моего поиска сбила с толку меня статья где-то на каком-то зарубежном сайте. Там говорилось, что необходимо взять сертификат формата *.pem, импортировать его в файл-хранилище, например типа bks,закинуть на Android-устройство и им пользоваться. И вот тут-то я свернула с верного пути. И потратила много времени на поиск и на различные тестовые реализации. Но как говорится - "Если долго мучиться что-нибудь получится" и мой кропотливый труд наконец-то был вознагражден! Спасибо, конечно, некоторым прекрасным лицам на форуме и на работе, которые помогали и подсказывали мне. И конечно спасибо удаче, которая посетила меня вчера. В итоге весь мой полуторонедельный труд уложился вот в этот кусочек кода:
private void onClickConnect(String path,String password,String https_url) {  
     //Создание файл хранилища для сертификата формата p12 
     //https_url - то, куда будем ходить 
     //path - путь к файлу на андройд-устройстве
     //password - пароль от сертификата. На многих сайтах пишут, 
     //чтобы пароль от файл-хранилища был такой же как и у сертификата.
            //Иначе будет ошибка.
     InputStream in = null;
     try {
  in = new FileInputStream(path);
     } catch (FileNotFoundException e1) {
  e1.printStackTrace();
     }
 
 
     //Фабрика клиентских сертификатов
     KeyManagerFactory mgrFact = null;
     try {
  mgrFact = KeyManagerFactory.getInstance("X509");
     } catch (NoSuchAlgorithmException e1) {
  e1.printStackTrace();
     }
 
     KeyStore clientStore = null;
     try {
  clientStore = KeyStore.getInstance("PKCS12");
     } catch (KeyStoreException e1) {
  e1.printStackTrace();
     }
 
     try {
      clientStore.load(in, password.toCharArray());
     } catch (NoSuchAlgorithmException e1) {
      e1.printStackTrace();
     } catch (CertificateException e1) {
      e1.printStackTrace();
     } catch (IOException e1) {
      e1.printStackTrace();
     }
 
     try {
  mgrFact.init(clientStore, password.toCharArray());
     } catch (UnrecoverableKeyException e1) {
  e1.printStackTrace();
     } catch (KeyStoreException e1) {
  e1.printStackTrace();
     } catch (NoSuchAlgorithmException e1) {
  e1.printStackTrace();
     }
 
            //Фабрика управления доверительными соединениями
     //она будет использоваться для проверки сертифкатов 
            //всех https соединений 
     final TrustManager[] trustAllCerts = new TrustManager[] {new     
                  X509TrustManager() {
         public X509Certificate[] getAcceptedIssuers() {
            System.out.println("getAcceptedIssuers");
            return null;
         }
 
         public void checkServerTrusted(X509Certificate[] chain, String authType)
             throws CertificateException {
             System.out.println("Сведения о сертификате : " + 
                    chain[0].getIssuerX500Principal().getName() +
                    " Тип авторизации : " + authType);
         }
 
         public void checkClientTrusted(X509Certificate[] chain, String authType)
      throws CertificateException {
           System.out.println("checkClientTrusted : " + authType);
         }
     } };
 
 
     //Далее создаем sslContext
     SSLContext sslContext = null;
     try {
  sslContext = SSLContext.getInstance("TLS");
     } catch (NoSuchAlgorithmException e) {
  e.printStackTrace();
     }
 
            //Настраиваем его с помощью наших фабрик
     try {
  sslContext.init(mgrFact.getKeyManagers(),trustAllCerts, new     
                java.security.SecureRandom());
     } catch (KeyManagementException e) {
  e.printStackTrace();
     }  
 
         final javax.net.ssl.SSLSocketFactory sslSocketFactory = 
                sslContext.getSocketFactory();
 
     //Создаем соединение
      HttpURLConnection conn = null;   
     try {
   conn = (HttpURLConnection) new URL(https_url).openConnection();
     } catch (MalformedURLException e) {
  e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     }
 
     //Настраиваем его
     try {
  conn.setRequestMethod("GET");
  conn.setUseCaches(false);
  conn.setDoInput(true);
  conn.setReadTimeout(10000 );
  conn.setConnectTimeout(15000);   
      } catch (ProtocolException e) {
  e.printStackTrace();
      }
 
      //encodeBase64 - функция преобразует строку в Base64
             //это если еще требуется авторизация
      conn.setRequestProperty("Authorization", "Basic  
                "+encodeBase64("user:password") ); 
 
             conn.setRequestProperty("Connection", "close");  
      conn.setRequestProperty("Accept-Charset", "cp1251"); 
      сonn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");  
      ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
 
      //Проверка имени хоста. Но проверку мы не делаем, а подтверждаем любой хост.
             //Кому надо, могут сделать проверку на что-нибудь
      ((HttpsURLConnection) conn).setHostnameVerifier(new HostnameVerifier() {
   public boolean verify(String arg0, SSLSession arg1) {
      return true;
  }
       });  
 
       // Устанавливаем соединение
       try {
  if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
   System.out.println("Ошибка соединения"+ conn.getResponseCode());
  }else{
   System.out.println("Соединение состоялось "+conn.getResponseCode()+
                        " ["+conn.getResponseMessage()+"]");
 
   StringBuffer buf = new StringBuffer();
   InputStreamReader in = new 
                             InputStreamReader(conn.getInputStream(), "cp1251");
   int ch;
   while ((ch = in.read()) != -1) {
    buf.append((char)c);
   }
   in.close();
 
   String response = buf.toString();
                 System.out.println("response "+response);
   //вывод различной информации по сертифкату, если надо.
   System.out.println("Cipher Suite : " + conn.getCipherSuite());
   Certificate[] certs = conn.getServerCertificates();
   System.out.println("n");
   for(Certificate cert : certs){
       System.out.println("Cert Type : " + cert.getType());
       System.out.println("Cert Hash Code : " + cert.hashCode());
       System.out.println("Cert Public Key Algorithm : " + 
                               cert.getPublicKey().getAlgorithm());
       System.out.println("Cert Public Key Format : " + 
                               cert.getPublicKey().getFormat());
   }    
  }
       } catch (IOException e) {
  e.printStackTrace();
       } catch (Exception e) {
  e.printStackTrace();
       }  
 }

 Это написано все в обработчике кнопки. Но желательно вынести все в отдельные функции или даже класс, что я реализую для себя сегодня )
 Спасибо Дмитрию и obrazel за их помощь, а так же моим коллегам.
Полезные статьи:
http://my-it-notes.com/2011/09/how-to-approve-ssl-certificate-on-android/ ,
http://www.cyberforum.ru/android-dev/thread613608-page2.html ,
http://www.javadocexamples.com/java/security/KeyStore/getInstance%28String%20type%29.html - вот эта полезная статья, которая наконец-то пнула меня в верном направлении и дала завершить проект!
 Если будут какие-то замечания по коду или предложения - буду рада услышать и узнать что-то новенькое!